From c6c7734c29e8a8bd0873054ac191cd2b5b77ec9a Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 11 Oct 2023 22:01:01 +0200 Subject: [PATCH 01/61] Merge pull request #6496 from appwrite/fix-readme-cn Update logo in README-CN.md ## What does this PR do? (Provide a description of what this PR does and why it's needed.) ## Test Plan (Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Screenshots may also be helpful.) ## Related PRs and Issues - (Related PR or issue) ## Checklist - [ ] Have you read the [Contributing Guidelines on issues](https://github.com/appwrite/appwrite/blob/master/CONTRIBUTING.md)? - [ ] If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs? --- app/http.php | 23 +++++- app/init.php | 76 +++++++++++-------- app/test | 184 ++++++++++++++++++++++++++++++++++++++++++++++ composer.lock | 22 +++--- package 2.json | 8 ++ package-lock.json | 10 +++ pnpm-lock.yaml | 5 ++ 7 files changed, 284 insertions(+), 44 deletions(-) create mode 100644 app/test create mode 100644 package 2.json create mode 100644 package-lock.json create mode 100644 pnpm-lock.yaml diff --git a/app/http.php b/app/http.php index fe1ed48724..72b89130b9 100644 --- a/app/http.php +++ b/app/http.php @@ -328,7 +328,28 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $swooleResponse->end(\json_encode($output)); } finally { - $pools->reclaim(); + $connectionForConsole = $app->getResource('connectionForConsole'); + $connectionForProject = $app->getResource('connectionForProject'); + $connectionForQueue = $app->getResource('connectionForQueue'); + $connectionsForCache = $app->getResource('connectionsForCache'); + + if(!is_null($connectionForConsole)) { + $connectionForConsole->reclaim(); + } + + if(!is_null($connectionForProject)) { + $connectionForProject->reclaim(); + } + + if(!is_null($connectionForQueue)) { + $connectionForQueue->reclaim(); + } + + if(!empty($connectionsForCache)) { + foreach ($connectionsForCache as $connection) { + $connection->reclaim(); + } + } } }); diff --git a/app/init.php b/app/init.php index ba133ec7fb..7078f90870 100644 --- a/app/init.php +++ b/app/init.php @@ -893,12 +893,6 @@ App::setResource('mails', fn() => new Mail()); App::setResource('deletes', fn() => new Delete()); App::setResource('database', fn() => new EventDatabase()); App::setResource('messaging', fn() => new Phone()); -App::setResource('queue', function (Group $pools) { - return $pools->get('queue')->pop()->getResource(); -}, ['pools']); -App::setResource('queueForFunctions', function (Connection $queue) { - return new Func($queue); -}, ['queue']); App::setResource('usage', function ($register) { return new Stats($register->get('statsd')); }, ['register']); @@ -1091,17 +1085,25 @@ App::setResource('console', function () { ]); }, []); +App::setResource('connectionForProject', function () { return null; }, []); +App::setResource('connectionForConsole', function () { return null; }, []); +App::setResource('connectionForQueue', function () { return null; }, []); +App::setResource('connectionsForCache', function () { return []; }, []); + App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForConsole; } - $dbAdapter = $pools + $connection = $pools ->get($project->getAttribute('database')) ->pop() - ->getResource() ; + App::setResource('connectionForProject', function () use ($connection) { return $connection; }, []); + + $dbAdapter = $connection->getResource(); + $database = new Database($dbAdapter, $cache); $database->setNamespace('_' . $project->getInternalId()); @@ -1109,11 +1111,13 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, }, ['pools', 'dbForConsole', 'cache', 'project']); App::setResource('dbForConsole', function (Group $pools, Cache $cache) { - $dbAdapter = $pools + $connection = $pools ->get('console') - ->pop() - ->getResource() - ; + ->pop(); + + App::setResource('connectionForConsole', function () use ($connection) { return $connection; }, []); + + $dbAdapter = $connection->getResource(); $database = new Database($dbAdapter, $cache); @@ -1123,30 +1127,22 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) { }, ['pools', 'cache']); App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { - $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - $getProjectDB = function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForConsole; } - - $databaseName = $project->getAttribute('database'); - - if (isset($databases[$databaseName])) { - $database = $databases[$databaseName]; - $database->setNamespace('_' . $project->getInternalId()); - return $database; - } - - $dbAdapter = $pools - ->get($databaseName) + + $connection = $pools + ->get($project->getAttribute('database')) ->pop() - ->getResource(); + ; + + $dbAdapter = $connection->getResource(); + + App::setResource('connectionForProject', function () use ($connection) { return $connection; }, []); $database = new Database($dbAdapter, $cache); - $databases[$databaseName] = $database; - $database->setNamespace('_' . $project->getInternalId()); return $database; @@ -1155,18 +1151,34 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, return $getProjectDB; }, ['pools', 'dbForConsole', 'cache']); +App::setResource('queue', function (Group $pools) { + $connection = $pools->get('queue')->pop(); + + App::setResource('connectionForQueue', function () use ($connection) { return $connection; }, []); + + return $connection->getResource(); +}, ['pools']); + +App::setResource('queueForFunctions', function (Connection $queue) { + return new Func($queue); +}, ['queue']); + App::setResource('cache', function (Group $pools) { $list = Config::getParam('pools-cache', []); $adapters = []; + $connections = []; foreach ($list as $value) { - $adapters[] = $pools + $connection = $pools ->get($value) - ->pop() - ->getResource() - ; + ->pop(); + + $connections[] = $connection; + $adapters[] = $connection->getResource(); } + App::setResource('connectionsForCache', function () use ($connections) { return $connections; }, []); + return new Cache(new Sharding($adapters)); }, ['pools']); diff --git a/app/test b/app/test new file mode 100644 index 0000000000..a4d28b1200 --- /dev/null +++ b/app/test @@ -0,0 +1,184 @@ +$register->set('pools', function () { + $group = new Group(); + + $fallbackForDB = 'db_main=' . AppwriteURL::unparse([ + 'scheme' => 'mariadb', + 'host' => App::getEnv('_APP_DB_HOST', 'mariadb'), + 'port' => App::getEnv('_APP_DB_PORT', '3306'), + 'user' => App::getEnv('_APP_DB_USER', ''), + 'pass' => App::getEnv('_APP_DB_PASS', ''), + 'path' => App::getEnv('_APP_DB_SCHEMA', ''), + ]); + $fallbackForRedis = 'redis_main=' . AppwriteURL::unparse([ + 'scheme' => 'redis', + 'host' => App::getEnv('_APP_REDIS_HOST', 'redis'), + 'port' => App::getEnv('_APP_REDIS_PORT', '6379'), + 'user' => App::getEnv('_APP_REDIS_USER', ''), + 'pass' => App::getEnv('_APP_REDIS_PASS', ''), + ]); + + $connections = [ + 'console' => [ + 'type' => 'database', + 'dsns' => App::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB), + 'multiple' => false, + 'schemes' => ['mariadb', 'mysql'], + ], + 'database' => [ + 'type' => 'database', + 'dsns' => App::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB), + 'multiple' => true, + 'schemes' => ['mariadb', 'mysql'], + ], + 'queue' => [ + 'type' => 'queue', + 'dsns' => App::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis), + 'multiple' => false, + 'schemes' => ['redis'], + ], + 'pubsub' => [ + 'type' => 'pubsub', + 'dsns' => App::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis), + 'multiple' => false, + 'schemes' => ['redis'], + ], + 'cache' => [ + 'type' => 'cache', + 'dsns' => App::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis), + 'multiple' => true, + 'schemes' => ['redis'], + ], + ]; + + $maxConnections = App::getEnv('_APP_CONNECTIONS_MAX', 151); + $instanceConnections = $maxConnections / App::getEnv('_APP_POOL_CLIENTS', 14); + + $multiprocessing = App::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled'; + + if ($multiprocessing) { + $workerCount = swoole_cpu_num() * intval(App::getEnv('_APP_WORKER_PER_CORE', 6)); + } else { + $workerCount = 1; + } + + if ($workerCount > $instanceConnections) { + throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500); + } + + $poolSize = (int)($instanceConnections / $workerCount); + + foreach ($connections as $key => $connection) { + $type = $connection['type'] ?? ''; + $dsns = $connection['dsns'] ?? ''; + $multipe = $connection['multiple'] ?? false; + $schemes = $connection['schemes'] ?? []; + $config = []; + $dsns = explode(',', $connection['dsns'] ?? ''); + foreach ($dsns as &$dsn) { + $dsn = explode('=', $dsn); + $name = ($multipe) ? $key . '_' . $dsn[0] : $key; + $dsn = $dsn[1] ?? ''; + $config[] = $name; + if (empty($dsn)) { + //throw new Exception(Exception::GENERAL_SERVER_ERROR, "Missing value for DSN connection in {$key}"); + continue; + } + + $dsn = new DSN($dsn); + $dsnHost = $dsn->getHost(); + $dsnPort = $dsn->getPort(); + $dsnUser = $dsn->getUser(); + $dsnPass = $dsn->getPassword(); + $dsnScheme = $dsn->getScheme(); + $dsnDatabase = $dsn->getPath(); + + if (!in_array($dsnScheme, $schemes)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid console database scheme"); + } + + /** + * Get Resource + * + * Creation could be reused accross connection types like database, cache, queue, etc. + * + * Resource assignment to an adapter will happen below. + */ + switch ($dsnScheme) { + case 'mysql': + case 'mariadb': + $resource = function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { + return new PDOProxy(function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { + return new PDO("mysql:host={$dsnHost};port={$dsnPort};dbname={$dsnDatabase};charset=utf8mb4", $dsnUser, $dsnPass, array( + PDO::ATTR_TIMEOUT => 3, // Seconds + PDO::ATTR_PERSISTENT => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed + PDO::ATTR_EMULATE_PREPARES => true, + PDO::ATTR_STRINGIFY_FETCHES => true + )); + }); + }; + break; + case 'redis': + $resource = function () use ($dsnHost, $dsnPort, $dsnPass) { + $redis = new Redis(); + @$redis->pconnect($dsnHost, (int)$dsnPort); + if ($dsnPass) { + $redis->auth($dsnPass); + } + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + + return $redis; + }; + break; + + default: + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid scheme"); + break; + } + + $pool = new Pool($name, $poolSize, function () use ($type, $resource, $dsn) { + // Get Adapter + $adapter = null; + switch ($type) { + case 'database': + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($resource()), + 'mysql' => new MySQL($resource()), + default => null + }; + + $adapter->setDefaultDatabase($dsn->getPath()); + break; + case 'pubsub': + $adapter = $resource(); + break; + case 'queue': + $adapter = match ($dsn->getScheme()) { + 'redis' => new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()), + default => null + }; + break; + case 'cache': + $adapter = match ($dsn->getScheme()) { + 'redis' => new RedisCache($resource()), + default => null + }; + break; + + default: + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation."); + break; + } + + return $adapter; + }); + + $group->add($pool); + } + + Config::setParam('pools-' . $key, $config); + } + + return $group; +}); diff --git a/composer.lock b/composer.lock index 288a17101f..27712b38ac 100644 --- a/composer.lock +++ b/composer.lock @@ -1050,16 +1050,16 @@ }, { "name": "matomo/device-detector", - "version": "6.1.5", + "version": "6.1.6", "source": { "type": "git", "url": "https://github.com/matomo-org/device-detector.git", - "reference": "40ca2990dba2c1719e5c62168e822e0b86c167d4" + "reference": "5cbea85106e561c7138d03603eb6e05128480409" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/40ca2990dba2c1719e5c62168e822e0b86c167d4", - "reference": "40ca2990dba2c1719e5c62168e822e0b86c167d4", + "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/5cbea85106e561c7138d03603eb6e05128480409", + "reference": "5cbea85106e561c7138d03603eb6e05128480409", "shasum": "" }, "require": { @@ -1115,7 +1115,7 @@ "source": "https://github.com/matomo-org/matomo", "wiki": "https://dev.matomo.org/" }, - "time": "2023-08-17T16:17:41+00:00" + "time": "2023-10-02T10:01:54+00:00" }, { "name": "mongodb/mongodb", @@ -2152,16 +2152,16 @@ }, { "name": "utopia-php/database", - "version": "0.43.4", + "version": "0.43.5", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "cabdd02e8dc1732eb0b22007c511e7bb3caa5c8c" + "reference": "5f7b05189cfbcc0506090498c580c5765375a00a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/cabdd02e8dc1732eb0b22007c511e7bb3caa5c8c", - "reference": "cabdd02e8dc1732eb0b22007c511e7bb3caa5c8c", + "url": "https://api.github.com/repos/utopia-php/database/zipball/5f7b05189cfbcc0506090498c580c5765375a00a", + "reference": "5f7b05189cfbcc0506090498c580c5765375a00a", "shasum": "" }, "require": { @@ -2202,9 +2202,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.43.4" + "source": "https://github.com/utopia-php/database/tree/0.43.5" }, - "time": "2023-09-28T09:00:05+00:00" + "time": "2023-10-06T06:49:47+00:00" }, { "name": "utopia-php/domains", diff --git a/package 2.json b/package 2.json new file mode 100644 index 0000000000..6e32c7d515 --- /dev/null +++ b/package 2.json @@ -0,0 +1,8 @@ +{ + "private": true, + "name": "@appwrite.io/repo", + "repository": { + "type": "git", + "url": "git+https://github.com/appwrite/appwrite.git" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..6ab33b14fe --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "@appwrite.io/repo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@appwrite.io/repo" + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000..2b9f1883a1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false From 0ea1418132b194c173e6efe211042e985c73b075 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 21 Oct 2023 06:57:59 -0400 Subject: [PATCH 02/61] Sync with main --- .../examples/health/get-queue-builds.md | 18 - .../examples/health/get-queue-databases.md | 18 - .../examples/health/get-queue-deletes.md | 18 - .../examples/health/get-queue-mails.md | 18 - .../Platform/Workers/Certificates.php | 534 --------------- src/Appwrite/Platform/Workers/Databases.php | 622 ------------------ src/Appwrite/Platform/Workers/Mails.php | 126 ---- src/Appwrite/Platform/Workers/Migrations.php | 330 ---------- src/Appwrite/Platform/Workers/Webhooks.php | 118 ---- 9 files changed, 1802 deletions(-) delete mode 100644 docs/examples/1.4.x/console-web/examples/health/get-queue-builds.md delete mode 100644 docs/examples/1.4.x/console-web/examples/health/get-queue-databases.md delete mode 100644 docs/examples/1.4.x/console-web/examples/health/get-queue-deletes.md delete mode 100644 docs/examples/1.4.x/console-web/examples/health/get-queue-mails.md delete mode 100644 src/Appwrite/Platform/Workers/Certificates.php delete mode 100644 src/Appwrite/Platform/Workers/Databases.php delete mode 100644 src/Appwrite/Platform/Workers/Mails.php delete mode 100644 src/Appwrite/Platform/Workers/Migrations.php delete mode 100644 src/Appwrite/Platform/Workers/Webhooks.php diff --git a/docs/examples/1.4.x/console-web/examples/health/get-queue-builds.md b/docs/examples/1.4.x/console-web/examples/health/get-queue-builds.md deleted file mode 100644 index a312fa7df0..0000000000 --- a/docs/examples/1.4.x/console-web/examples/health/get-queue-builds.md +++ /dev/null @@ -1,18 +0,0 @@ -import { Client, Health } from "@appwrite.io/console"; - -const client = new Client(); - -const health = new Health(client); - -client - .setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint - .setProject('5df5acd0d48c2') // Your project ID -; - -const promise = health.getQueueBuilds(); - -promise.then(function (response) { - console.log(response); // Success -}, function (error) { - console.log(error); // Failure -}); \ No newline at end of file diff --git a/docs/examples/1.4.x/console-web/examples/health/get-queue-databases.md b/docs/examples/1.4.x/console-web/examples/health/get-queue-databases.md deleted file mode 100644 index 481480a80d..0000000000 --- a/docs/examples/1.4.x/console-web/examples/health/get-queue-databases.md +++ /dev/null @@ -1,18 +0,0 @@ -import { Client, Health } from "@appwrite.io/console"; - -const client = new Client(); - -const health = new Health(client); - -client - .setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint - .setProject('5df5acd0d48c2') // Your project ID -; - -const promise = health.getQueueDatabases(); - -promise.then(function (response) { - console.log(response); // Success -}, function (error) { - console.log(error); // Failure -}); \ No newline at end of file diff --git a/docs/examples/1.4.x/console-web/examples/health/get-queue-deletes.md b/docs/examples/1.4.x/console-web/examples/health/get-queue-deletes.md deleted file mode 100644 index c1bbb9f655..0000000000 --- a/docs/examples/1.4.x/console-web/examples/health/get-queue-deletes.md +++ /dev/null @@ -1,18 +0,0 @@ -import { Client, Health } from "@appwrite.io/console"; - -const client = new Client(); - -const health = new Health(client); - -client - .setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint - .setProject('5df5acd0d48c2') // Your project ID -; - -const promise = health.getQueueDeletes(); - -promise.then(function (response) { - console.log(response); // Success -}, function (error) { - console.log(error); // Failure -}); \ No newline at end of file diff --git a/docs/examples/1.4.x/console-web/examples/health/get-queue-mails.md b/docs/examples/1.4.x/console-web/examples/health/get-queue-mails.md deleted file mode 100644 index 8d6d68228a..0000000000 --- a/docs/examples/1.4.x/console-web/examples/health/get-queue-mails.md +++ /dev/null @@ -1,18 +0,0 @@ -import { Client, Health } from "@appwrite.io/console"; - -const client = new Client(); - -const health = new Health(client); - -client - .setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint - .setProject('5df5acd0d48c2') // Your project ID -; - -const promise = health.getQueueMails(); - -promise.then(function (response) { - console.log(response); // Success -}, function (error) { - console.log(error); // Failure -}); \ No newline at end of file diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php deleted file mode 100644 index 02c1835dd5..0000000000 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ /dev/null @@ -1,534 +0,0 @@ -desc('Certificates worker') - ->inject('message') - ->inject('dbForConsole') - ->inject('queueForMails') - ->inject('queueForEvents') - ->inject('queueForFunctions') - ->callback(fn(Message $message, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions) => $this->action($message, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions)); - } - - /** - * @param Message $message - * @param Database $dbForConsole - * @param Mail $queueForMails - * @param Event $queueForEvents - * @param Func $queueForFunctions - * @return void - * @throws Throwable - * @throws \Utopia\Database\Exception - */ - public function action(Message $message, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions): void - { - $payload = $message->getPayload() ?? []; - - if (empty($payload)) { - throw new Exception('Missing payload'); - } - - $document = new Document($payload['domain'] ?? []); - $domain = new Domain($document->getAttribute('domain', '')); - $skipRenewCheck = $payload['skipRenewCheck'] ?? false; - - $this->execute($domain, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $skipRenewCheck); - } - - /** - * @param Domain $domain - * @param Database $dbForConsole - * @param Mail $queueForMails - * @param Event $queueForEvents - * @param Func $queueForFunctions - * @param bool $skipRenewCheck - * @return void - * @throws Throwable - * @throws \Utopia\Database\Exception - */ - private function execute(Domain $domain, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, bool $skipRenewCheck = false): void - { - /** - * 1. Read arguments and validate domain - * 2. Get main domain - * 3. Validate CNAME DNS if parameter is not main domain (meaning it's custom domain) - * 4. Validate security email. Cannot be empty, required by LetsEncrypt - * 5. Validate renew date with certificate file, unless requested to skip by parameter - * 6. Issue a certificate using certbot CLI - * 7. Update 'log' attribute on certificate document with Certbot message - * 8. Create storage folder for certificate, if not ready already - * 9. Move certificates from Certbot location to our Storage - * 10. Create/Update our Storage with new Traefik config with new certificate paths - * 11. Read certificate file and update 'renewDate' on certificate document - * 12. Update 'issueDate' and 'attempts' on certificate - * - * If at any point unexpected error occurs, program stops without applying changes to document, and error is thrown into worker - * - * If code stops with expected error: - * 1. 'log' attribute on document is updated with error message - * 2. 'attempts' amount is increased - * 3. Console log is shown - * 4. Email is sent to security email - * - * Unless unexpected error occurs, at the end, we: - * 1. Update 'updated' attribute on document - * 2. Save document to database - * 3. Update all domains documents with current certificate ID - * - * Note: Renewals are checked and scheduled from maintenence worker - */ - - // Get current certificate - $certificate = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain->get()])]); - - // If we don't have certificate for domain yet, let's create new document. At the end we save it - if (!$certificate) { - $certificate = new Document(); - $certificate->setAttribute('domain', $domain->get()); - } - - $success = false; - - try { - // Email for alerts is required by LetsEncrypt - $email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'); - if (empty($email)) { - throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate.'); - } - - // Validate domain and DNS records. Skip if job is forced - if (!$skipRenewCheck) { - $mainDomain = $this->getMainDomain(); - $isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain; - $this->validateDomain($domain, $isMainDomain); - } - - // If certificate exists already, double-check expiry date. Skip if job is forced - if (!$skipRenewCheck && !$this->isRenewRequired($domain->get())) { - throw new Exception('Renew isn\'t required.'); - } - - // Prepare folder name for certbot. Using this helps prevent miss-match in LetsEncrypt configuration when renewing certificate - $folder = ID::unique(); - - // Generate certificate files using Let's Encrypt - $letsEncryptData = $this->issueCertificate($folder, $domain->get(), $email); - - // Command succeeded, store all data into document - $logs = 'Certificate successfully generated.'; - $certificate->setAttribute('logs', \mb_strcut($logs, 0, 1000000));// Limit to 1MB - - - // Give certificates to Traefik - $this->applyCertificateFiles($folder, $domain->get(), $letsEncryptData); - - // Update certificate info stored in database - $certificate->setAttribute('renewDate', $this->getRenewDate($domain->get())); - $certificate->setAttribute('attempts', 0); - $certificate->setAttribute('issueDate', DateTime::now()); - $success = true; - } catch (Throwable $e) { - $logs = $e->getMessage(); - - // Set exception as log in certificate document - $certificate->setAttribute('logs', \mb_strcut($logs, 0, 1000000));// Limit to 1MB - - // Increase attempts count - $attempts = $certificate->getAttribute('attempts', 0) + 1; - $certificate->setAttribute('attempts', $attempts); - - // Store cuttent time as renew date to ensure another attempt in next maintenance cycle - $certificate->setAttribute('renewDate', DateTime::now()); - - // Send email to security email - $this->notifyError($domain->get(), $e->getMessage(), $attempts, $queueForMails); - } finally { - // All actions result in new updatedAt date - $certificate->setAttribute('updated', DateTime::now()); - - // Save all changes we made to certificate document into database - $this->saveCertificateDocument($domain->get(), $certificate, $success, $dbForConsole, $queueForEvents, $queueForFunctions); - } - } - - /** - * Save certificate data into database. - * - * @param string $domain Domain name that certificate is for - * @param Document $certificate Certificate document that we need to save - * @param bool $success - * @param Database $dbForConsole Database connection for console - * @param Event $queueForEvents - * @param Func $queueForFunctions - * @return void - * @throws \Utopia\Database\Exception - * @throws Authorization - * @throws Conflict - * @throws Structure - */ - private function saveCertificateDocument(string $domain, Document $certificate, bool $success, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions): void - { - // Check if update or insert required - $certificateDocument = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain])]); - if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) { - // Merge new data with current data - $certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy())); - $certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate); - } else { - $certificate->removeAttribute('$internalId'); - $certificate = $dbForConsole->createDocument('certificates', $certificate); - } - - $certificateId = $certificate->getId(); - $this->updateDomainDocuments($certificateId, $domain, $success, $dbForConsole, $queueForEvents, $queueForFunctions); - } - - /** - * Get main domain. Needed as we do different checks for main and non-main domains. - * - * @return null|string Returns main domain. If null, there is no main domain yet. - */ - private function getMainDomain(): ?string - { - $envDomain = App::getEnv('_APP_DOMAIN', ''); - if (!empty($envDomain) && $envDomain !== 'localhost') { - return $envDomain; - } - - return null; - } - - /** - * Internal domain validation functionality to prevent unnecessary attempts failed from Let's Encrypt side. We check: - * - Domain needs to be public and valid (prevents NFT domains that are not supported by Let's Encrypt) - * - Domain must have proper DNS record - * - * @param Domain $domain Domain which we validate - * @param bool $isMainDomain In case of master domain, we look for different DNS configurations - * - * @return void - * @throws Exception - */ - private function validateDomain(Domain $domain, bool $isMainDomain): void - { - if (empty($domain->get())) { - throw new Exception('Missing certificate domain.'); - } - - if (!$domain->isKnown() || $domain->isTest()) { - throw new Exception('Unknown public suffix for domain.'); - } - - if (!$isMainDomain) { - // TODO: Would be awesome to also support A/AAAA records here. Maybe dry run? - // Validate if domain target is properly configured - $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); - - if (!$target->isKnown() || $target->isTest()) { - throw new Exception('Unreachable CNAME target (' . $target->get() . '), please use a domain with a public suffix.'); - } - - // Verify domain with DNS records - $validator = new CNAME($target->get()); - if (!$validator->isValid($domain->get())) { - throw new Exception('Failed to verify domain DNS records.'); - } - } else { - // Main domain validation - // TODO: Would be awesome to check A/AAAA record here. Maybe dry run? - } - } - - /** - * Reads expiry date of certificate from file and decides if renewal is required or not. - * - * @param string $domain Domain for which we check certificate file - * @return bool True, if certificate needs to be renewed - * @throws Exception - */ - private function isRenewRequired(string $domain): bool - { - $certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem'; - if (\file_exists($certPath)) { - $validTo = null; - - $certData = openssl_x509_parse(file_get_contents($certPath)); - $validTo = $certData['validTo_time_t'] ?? 0; - - if (empty($validTo)) { - throw new Exception('Unable to read certificate file (cert.pem).'); - } - - // LetsEncrypt allows renewal 30 days before expiry - $expiryInAdvance = (60 * 60 * 24 * 30); - if ($validTo - $expiryInAdvance > \time()) { - return false; - } - } - - return true; - } - - /** - * LetsEncrypt communication to issue certificate (using certbot CLI) - * - * @param string $folder Folder into which certificates should be generated - * @param string $domain Domain to generate certificate for - * @return array Named array with keys 'stdout' and 'stderr', both string - * @throws Exception - */ - private function issueCertificate(string $folder, string $domain, string $email): array - { - $stdout = ''; - $stderr = ''; - - $staging = (App::isProduction()) ? '' : ' --dry-run'; - $exit = Console::execute("certbot certonly -v --webroot --noninteractive --agree-tos{$staging}" - . " --email " . $email - . " --cert-name " . $folder - . " -w " . APP_STORAGE_CERTIFICATES - . " -d {$domain}", '', $stdout, $stderr); - - // Unexpected error, usually 5XX, API limits, ... - if ($exit !== 0) { - throw new Exception('Failed to issue a certificate with message: ' . $stderr); - } - - return [ - 'stdout' => $stdout, - 'stderr' => $stderr - ]; - } - - /** - * Read new renew date from certificate file generated by Let's Encrypt - * - * @param string $domain Domain which certificate was generated for - * @return string - * @throws \Utopia\Database\Exception - */ - private function getRenewDate(string $domain): string - { - $certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem'; - $certData = openssl_x509_parse(file_get_contents($certPath)); - $validTo = $certData['validTo_time_t'] ?? null; - $dt = (new \DateTime())->setTimestamp($validTo); - return DateTime::addSeconds($dt, -60 * 60 * 24 * 30); // -30 days - } - - /** - * Method to take files from Let's Encrypt, and put it into Traefik. - * - * @param string $domain Domain which certificate was generated for - * @param string $folder Folder in which certificates were generated - * @param array $letsEncryptData Let's Encrypt logs to use for additional info when throwing error - * @return void - * @throws Exception - */ - private function applyCertificateFiles(string $folder, string $domain, array $letsEncryptData): void - { - - // Prepare folder in storage for domain - $path = APP_STORAGE_CERTIFICATES . '/' . $domain; - if (!\is_readable($path)) { - if (!\mkdir($path, 0755, true)) { - throw new Exception('Failed to create path for certificate.'); - } - } - - // Move generated files - if (!@\rename('/etc/letsencrypt/live/' . $folder . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) { - throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); - } - - if (!@\rename('/etc/letsencrypt/live/' . $folder . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) { - throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); - } - - if (!@\rename('/etc/letsencrypt/live/' . $folder . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) { - throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); - } - - if (!@\rename('/etc/letsencrypt/live/' . $folder . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) { - throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); - } - - $config = \implode(PHP_EOL, [ - "tls:", - " certificates:", - " - certFile: /storage/certificates/{$domain}/fullchain.pem", - " keyFile: /storage/certificates/{$domain}/privkey.pem" - ]); - - // Save configuration into Traefik using our new cert files - if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain . '.yml', $config)) { - throw new Exception('Failed to save Traefik configuration.'); - } - } - - /** - * Method to make sure information about error is delivered to admnistrator. - * - * @param string $domain Domain that caused the error - * @param string $errorMessage Verbose error message - * @param int $attempt How many times it failed already - * @param Mail $queueForMails - * @return void - * @throws Exception - */ - private function notifyError(string $domain, string $errorMessage, int $attempt, Mail $queueForMails): void - { - // Log error into console - Console::warning('Cannot renew domain (' . $domain . ') on attempt no. ' . $attempt . ' certificate: ' . $errorMessage); - - // Send mail to administratore mail - - $locale = new Locale(App::getEnv('_APP_LOCALE', 'en')); - if (!$locale->getText('emails.sender') || !$locale->getText("emails.certificate.hello") || !$locale->getText("emails.certificate.subject") || !$locale->getText("emails.certificate.body") || !$locale->getText("emails.certificate.footer") || !$locale->getText("emails.certificate.thanks") || !$locale->getText("emails.certificate.signature")) { - $locale->setDefault('en'); - } - - $body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base.tpl'); - - $subject = \sprintf($locale->getText("emails.certificate.subject"), $domain); - $body - ->setParam('{{domain}}', $domain) - ->setParam('{{error}}', $errorMessage) - ->setParam('{{attempt}}', $attempt) - ->setParam('{{subject}}', $subject) - ->setParam('{{hello}}', $locale->getText("emails.certificate.hello")) - ->setParam('{{body}}', $locale->getText("emails.certificate.body")) - ->setParam('{{redirect}}', 'https://' . $domain) - ->setParam('{{footer}}', $locale->getText("emails.certificate.footer")) - ->setParam('{{thanks}}', $locale->getText("emails.certificate.thanks")) - ->setParam('{{signature}}', $locale->getText("emails.certificate.signature")) - ->setParam('{{project}}', 'Console') - ->setParam('{{direction}}', $locale->getText('settings.direction')) - ->setParam('{{bg-body}}', '#f7f7f7') - ->setParam('{{bg-content}}', '#ffffff') - ->setParam('{{text-content}}', '#000000'); - - $queueForMails - ->setRecipient(App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')) - ->setBody($body->render()) - ->setName('Appwrite Administrator') - ->trigger(); - } - - /** - * Update all existing domain documents so they have relation to correct certificate document. - * This solved issues: - * - when adding a domain for which there is already a certificate - * - when renew creates new document? It might? - * - overall makes it more reliable - * - * @param string $certificateId ID of a new or updated certificate document - * @param string $domain Domain that is affected by new certificate - * @param bool $success Was certificate generation successful? - * - * @return void - */ - private function updateDomainDocuments(string $certificateId, string $domain, bool $success, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions): void - { - - $rule = $dbForConsole->findOne('rules', [ - Query::equal('domain', [$domain]), - ]); - - if ($rule !== false && !$rule->isEmpty()) { - $rule->setAttribute('certificateId', $certificateId); - $rule->setAttribute('status', $success ? 'verified' : 'unverified'); - $dbForConsole->updateDocument('rules', $rule->getId(), $rule); - - $projectId = $rule->getAttribute('projectId'); - - // Skip events for console project (triggered by auto-ssl generation for 1 click setups) - if ($projectId === 'console') { - return; - } - - $project = $dbForConsole->getDocument('projects', $projectId); - - /** Trigger Webhook */ - $ruleModel = new Rule(); - $queueForEvents - ->setProject($project) - ->setEvent('rules.[ruleId].update') - ->setParam('ruleId', $rule->getId()) - ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) - ->trigger(); - - - /** Trigger Functions */ - $queueForFunctions - ->setProject($project) - ->setEvent('rules.[ruleId].update') - ->setParam('ruleId', $rule->getId()) - ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) - ->trigger(); - - /** Trigger realtime event */ - $allEvents = Event::generateEvents('rules.[ruleId].update', [ - 'ruleId' => $rule->getId(), - ]); - $target = Realtime::fromPayload( - // Pass first, most verbose event pattern - event: $allEvents[0], - payload: $rule, - project: $project - ); - Realtime::send( - projectId: 'console', - payload: $rule->getArrayCopy(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'] - ); - Realtime::send( - projectId: $project->getId(), - payload: $rule->getArrayCopy(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'] - ); - } - } -} diff --git a/src/Appwrite/Platform/Workers/Databases.php b/src/Appwrite/Platform/Workers/Databases.php deleted file mode 100644 index e0ec75e1d4..0000000000 --- a/src/Appwrite/Platform/Workers/Databases.php +++ /dev/null @@ -1,622 +0,0 @@ -desc('Databases worker') - ->inject('message') - ->inject('dbForConsole') - ->inject('dbForProject') - ->callback(fn($message, $dbForConsole, $dbForProject) => $this->action($message, $dbForConsole, $dbForProject)); - } - - /** - * @param Message $message - * @param Database $dbForConsole - * @param Database $dbForProject - * @return void - * @throws \Exception - */ - public function action(Message $message, Database $dbForConsole, Database $dbForProject): void - { - $payload = $message->getPayload() ?? []; - - if (empty($payload)) { - throw new \Exception('Missing payload'); - } - - $type = $payload['type']; - $project = new Document($payload['project']); - $collection = new Document($payload['collection'] ?? []); - $document = new Document($payload['document'] ?? []); - $database = new Document($payload['database'] ?? []); - - if ($database->isEmpty()) { - throw new Exception('Missing database'); - } - - match (strval($type)) { - DATABASE_TYPE_DELETE_DATABASE => $this->deleteDatabase($database, $project, $dbForProject), - DATABASE_TYPE_DELETE_COLLECTION => $this->deleteCollection($database, $collection, $project, $dbForProject), - DATABASE_TYPE_CREATE_ATTRIBUTE => $this->createAttribute($database, $collection, $document, $project, $dbForConsole, $dbForProject), - DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForConsole, $dbForProject), - DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForConsole, $dbForProject), - DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForConsole, $dbForProject), - default => Console::error('No database operation for type: ' . $type), - }; - } - - /** - * @param Document $database - * @param Document $collection - * @param Document $attribute - * @param Document $project - * @param Database $dbForConsole - * @param Database $dbForProject - * @return void - * @throws Authorization - * @throws Conflict - * @throws \Exception - */ - private function createAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForConsole, Database $dbForProject): void - { - if ($collection->isEmpty()) { - throw new Exception('Missing collection'); - } - if ($attribute->isEmpty()) { - throw new Exception('Missing attribute'); - } - - $projectId = $project->getId(); - - $events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].update', [ - 'databaseId' => $database->getId(), - 'collectionId' => $collection->getId(), - 'attributeId' => $attribute->getId() - ]); - /** - * TODO @christyjacob4 verify if this is still the case - * Fetch attribute from the database, since with Resque float values are loosing informations. - */ - $attribute = $dbForProject->getDocument('attributes', $attribute->getId()); - - $collectionId = $collection->getId(); - $key = $attribute->getAttribute('key', ''); - $type = $attribute->getAttribute('type', ''); - $size = $attribute->getAttribute('size', 0); - $required = $attribute->getAttribute('required', false); - $default = $attribute->getAttribute('default', null); - $signed = $attribute->getAttribute('signed', true); - $array = $attribute->getAttribute('array', false); - $format = $attribute->getAttribute('format', ''); - $formatOptions = $attribute->getAttribute('formatOptions', []); - $filters = $attribute->getAttribute('filters', []); - $options = $attribute->getAttribute('options', []); - $project = $dbForConsole->getDocument('projects', $projectId); - - - try { - switch ($type) { - case Database::VAR_RELATIONSHIP: - $relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']); - if ($relatedCollection->isEmpty()) { - throw new DatabaseException('Collection not found'); - } - - if ( - !$dbForProject->createRelationship( - collection: 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), - relatedCollection: 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), - type: $options['relationType'], - twoWay: $options['twoWay'], - id: $key, - twoWayKey: $options['twoWayKey'], - onDelete: $options['onDelete'], - ) - ) { - throw new DatabaseException('Failed to create Attribute'); - } - - if ($options['twoWay']) { - $relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']); - $dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'available')); - } - break; - default: - if (!$dbForProject->createAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters)) { - throw new \Exception('Failed to create Attribute'); - } - } - - $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available')); - } catch (\Exception $e) { - Console::error($e->getMessage()); - - if ($e instanceof DatabaseException) { - $attribute->setAttribute('error', $e->getMessage()); - if (isset($relatedAttribute)) { - $relatedAttribute->setAttribute('error', $e->getMessage()); - } - } - - $dbForProject->updateDocument( - 'attributes', - $attribute->getId(), - $attribute->setAttribute('status', 'failed') - ); - - if (isset($relatedAttribute)) { - $dbForProject->updateDocument( - 'attributes', - $relatedAttribute->getId(), - $relatedAttribute->setAttribute('status', 'failed') - ); - } - } finally { - $this->trigger($database, $collection, $attribute, $project, $projectId, $events); - } - - if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) { - $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId()); - } - - $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId); - } - - /** - * @param Document $database - * @param Document $collection - * @param Document $attribute - * @param Document $project - * @param Database $dbForConsole - * @param Database $dbForProject - * @return void - * @throws Authorization - * @throws Conflict - * @throws \Exception - **/ - private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForConsole, Database $dbForProject): void - { - if ($collection->isEmpty()) { - throw new Exception('Missing collection'); - } - if ($attribute->isEmpty()) { - throw new Exception('Missing attribute'); - } - - $projectId = $project->getId(); - - $events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete', [ - 'databaseId' => $database->getId(), - 'collectionId' => $collection->getId(), - 'attributeId' => $attribute->getId() - ]); - $collectionId = $collection->getId(); - $key = $attribute->getAttribute('key', ''); - $status = $attribute->getAttribute('status', ''); - $type = $attribute->getAttribute('type', ''); - $project = $dbForConsole->getDocument('projects', $projectId); - $options = $attribute->getAttribute('options', []); - $relatedAttribute = new Document(); - $relatedCollection = new Document(); - // possible states at this point: - // - available: should not land in queue; controller flips these to 'deleting' - // - processing: hasn't finished creating - // - deleting: was available, in deletion queue for first time - // - failed: attribute was never created - // - stuck: attribute was available but cannot be removed - - try { - if ($status !== 'failed') { - if ($type === Database::VAR_RELATIONSHIP) { - if ($options['twoWay']) { - $relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']); - if ($relatedCollection->isEmpty()) { - throw new DatabaseException('Collection not found'); - } - $relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']); - } - - if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) { - $dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck')); - throw new DatabaseException('Failed to delete Relationship'); - } - } elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) { - throw new DatabaseException('Failed to delete Attribute'); - } - } - - $dbForProject->deleteDocument('attributes', $attribute->getId()); - - if (!$relatedAttribute->isEmpty()) { - $dbForProject->deleteDocument('attributes', $relatedAttribute->getId()); - } - } catch (\Exception $e) { - Console::error($e->getMessage()); - - if ($e instanceof DatabaseException) { - $attribute->setAttribute('error', $e->getMessage()); - if (!$relatedAttribute->isEmpty()) { - $relatedAttribute->setAttribute('error', $e->getMessage()); - } - } - $dbForProject->updateDocument( - 'attributes', - $attribute->getId(), - $attribute->setAttribute('status', 'stuck') - ); - if (!$relatedAttribute->isEmpty()) { - $dbForProject->updateDocument( - 'attributes', - $relatedAttribute->getId(), - $relatedAttribute->setAttribute('status', 'stuck') - ); - } - } finally { - $this->trigger($database, $collection, $attribute, $project, $projectId, $events); - } - - // The underlying database removes/rebuilds indexes when attribute is removed - // Update indexes table with changes - /** @var Document[] $indexes */ - $indexes = $collection->getAttribute('indexes', []); - - foreach ($indexes as $index) { - /** @var string[] $attributes */ - $attributes = $index->getAttribute('attributes'); - $lengths = $index->getAttribute('lengths'); - $orders = $index->getAttribute('orders'); - - $found = \array_search($key, $attributes); - - if ($found !== false) { - // If found, remove entry from attributes, lengths, and orders - // array_values wraps array_diff to reindex array keys - // when found attribute is removed from array - $attributes = \array_values(\array_diff($attributes, [$attributes[$found]])); - $lengths = \array_values(\array_diff($lengths, isset($lengths[$found]) ? [$lengths[$found]] : [])); - $orders = \array_values(\array_diff($orders, isset($orders[$found]) ? [$orders[$found]] : [])); - - if (empty($attributes)) { - $dbForProject->deleteDocument('indexes', $index->getId()); - } else { - $index - ->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN) - ->setAttribute('lengths', $lengths, Document::SET_TYPE_ASSIGN) - ->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN); - - // Check if an index exists with the same attributes and orders - $exists = false; - foreach ($indexes as $existing) { - if ( - $existing->getAttribute('key') !== $index->getAttribute('key') // Ignore itself - && $existing->getAttribute('attributes') === $index->getAttribute('attributes') - && $existing->getAttribute('orders') === $index->getAttribute('orders') - ) { - $exists = true; - break; - } - } - - if ($exists) { // Delete the duplicate if created, else update in db - $this->deleteIndex($database, $collection, $index, $project, $dbForConsole, $dbForProject); - } else { - $dbForProject->updateDocument('indexes', $index->getId(), $index); - } - } - } - } - - $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId); - $dbForProject->deleteCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); - - if (!$relatedCollection->isEmpty() && !$relatedAttribute->isEmpty()) { - $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId()); - $dbForProject->deleteCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); - } - } - - /** - * @param Document $database - * @param Document $collection - * @param Document $index - * @param Document $project - * @param Database $dbForConsole - * @param Database $dbForProject - * @return void - * @throws Authorization - * @throws Conflict - * @throws Structure - * @throws DatabaseException - */ - private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForConsole, Database $dbForProject): void - { - if ($collection->isEmpty()) { - throw new Exception('Missing collection'); - } - if ($index->isEmpty()) { - throw new Exception('Missing index'); - } - - $projectId = $project->getId(); - - $events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].update', [ - 'databaseId' => $database->getId(), - 'collectionId' => $collection->getId(), - 'indexId' => $index->getId() - ]); - $collectionId = $collection->getId(); - $key = $index->getAttribute('key', ''); - $type = $index->getAttribute('type', ''); - $attributes = $index->getAttribute('attributes', []); - $lengths = $index->getAttribute('lengths', []); - $orders = $index->getAttribute('orders', []); - $project = $dbForConsole->getDocument('projects', $projectId); - - try { - if (!$dbForProject->createIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $attributes, $lengths, $orders)) { - throw new DatabaseException('Failed to create Index'); - } - $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available')); - } catch (\Exception $e) { - Console::error($e->getMessage()); - - if ($e instanceof DatabaseException) { - $index->setAttribute('error', $e->getMessage()); - } - $dbForProject->updateDocument( - 'indexes', - $index->getId(), - $index->setAttribute('status', 'failed') - ); - } finally { - $this->trigger($database, $collection, $index, $project, $projectId, $events); - } - - $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId); - } - - /** - * @param Document $database - * @param Document $collection - * @param Document $index - * @param Document $project - * @param Database $dbForConsole - * @param Database $dbForProject - * @return void - * @throws Authorization - * @throws Conflict - * @throws Structure - * @throws DatabaseException - */ - private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForConsole, Database $dbForProject): void - { - if ($collection->isEmpty()) { - throw new Exception('Missing collection'); - } - if ($index->isEmpty()) { - throw new Exception('Missing index'); - } - - $projectId = $project->getId(); - - $events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].delete', [ - 'databaseId' => $database->getId(), - 'collectionId' => $collection->getId(), - 'indexId' => $index->getId() - ]); - $key = $index->getAttribute('key'); - $status = $index->getAttribute('status', ''); - $project = $dbForConsole->getDocument('projects', $projectId); - - try { - if ($status !== 'failed' && !$dbForProject->deleteIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) { - throw new DatabaseException('Failed to delete index'); - } - $dbForProject->deleteDocument('indexes', $index->getId()); - $index->setAttribute('status', 'deleted'); - } catch (\Exception $e) { - Console::error($e->getMessage()); - - if ($e instanceof DatabaseException) { - $index->setAttribute('error', $e->getMessage()); - } - $dbForProject->updateDocument( - 'indexes', - $index->getId(), - $index->setAttribute('status', 'stuck') - ); - } finally { - $this->trigger($database, $collection, $index, $project, $projectId, $events); - } - - $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collection->getId()); - } - - /** - * @param Document $database - * @param Document $project - * @param $dbForProject - * @return void - * @throws Exception - */ - protected function deleteDatabase(Document $database, Document $project, $dbForProject): void - { - $this->deleteByGroup('database_' . $database->getInternalId(), [], $dbForProject, function ($collection) use ($database, $project, $dbForProject) { - $this->deleteCollection($database, $collection, $project, $dbForProject); - }); - - $dbForProject->deleteCollection('database_' . $database->getInternalId()); - - $this->deleteAuditLogsByResource('database/' . $database->getId(), $project, $dbForProject); - } - - /** - * @param Document $database - * @param Document $collection - * @param Document $project - * @param Database $dbForProject - * @return void - * @throws Authorization - * @throws Conflict - * @throws DatabaseException - * @throws Restricted - * @throws Structure - */ - protected function deleteCollection(Document $database, Document $collection, Document $project, Database $dbForProject): void - { - if ($collection->isEmpty()) { - throw new Exception('Missing collection'); - } - - $collectionId = $collection->getId(); - $collectionInternalId = $collection->getInternalId(); - $databaseId = $database->getId(); - $databaseInternalId = $database->getInternalId(); - - $relationships = \array_filter( - $collection->getAttribute('attributes'), - fn ($attribute) => $attribute['type'] === Database::VAR_RELATIONSHIP - ); - - foreach ($relationships as $relationship) { - if (!$relationship['twoWay']) { - continue; - } - $relatedCollection = $dbForProject->getDocument('database_' . $databaseInternalId, $relationship['relatedCollection']); - $dbForProject->deleteDocument('attributes', $databaseInternalId . '_' . $relatedCollection->getInternalId() . '_' . $relationship['twoWayKey']); - $dbForProject->deleteCachedDocument('database_' . $databaseInternalId, $relatedCollection->getId()); - $dbForProject->deleteCachedCollection('database_' . $databaseInternalId . '_collection_' . $relatedCollection->getInternalId()); - } - - $dbForProject->deleteCollection('database_' . $databaseInternalId . '_collection_' . $collection->getInternalId()); - - $this->deleteByGroup('attributes', [ - Query::equal('databaseInternalId', [$databaseInternalId]), - Query::equal('collectionInternalId', [$collectionInternalId]) - ], $dbForProject); - - $this->deleteByGroup('indexes', [ - Query::equal('databaseInternalId', [$databaseInternalId]), - Query::equal('collectionInternalId', [$collectionInternalId]) - ], $dbForProject); - - $this->deleteAuditLogsByResource('database/' . $databaseId . '/collection/' . $collectionId, $project, $dbForProject); - } - - /** - * @param string $resource - * @param Document $project - * @param Database $dbForProject - * @return void - * @throws Exception - */ - protected function deleteAuditLogsByResource(string $resource, Document $project, Database $dbForProject): void - { - $this->deleteByGroup(Audit::COLLECTION, [ - Query::equal('resource', [$resource]) - ], $dbForProject); - } - - /** - * @param string $collection collectionID - * @param array $queries - * @param Database $database - * @param callable|null $callback - * @return void - * @throws Exception - */ - protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null): void - { - $count = 0; - $chunk = 0; - $limit = 50; - $sum = $limit; - - $executionStart = \microtime(true); - - while ($sum === $limit) { - $chunk++; - - $results = $database->find($collection, \array_merge([Query::limit($limit)], $queries)); - - $sum = count($results); - - Console::info('Deleting chunk #' . $chunk . '. Found ' . $sum . ' documents'); - - foreach ($results as $document) { - if ($database->deleteDocument($document->getCollection(), $document->getId())) { - Console::success('Deleted document "' . $document->getId() . '" successfully'); - - if (\is_callable($callback)) { - $callback($document); - } - } else { - Console::error('Failed to delete document: ' . $document->getId()); - } - $count++; - } - } - - $executionEnd = \microtime(true); - - Console::info("Deleted {$count} document by group in " . ($executionEnd - $executionStart) . " seconds"); - } - - protected function trigger( - Document $database, - Document $collection, - Document $attribute, - Document $project, - string $projectId, - array $events - ): void { - $target = Realtime::fromPayload( - // Pass first, most verbose event pattern - event: $events[0], - payload: $attribute, - project: $project, - ); - Realtime::send( - projectId: 'console', - payload: $attribute->getArrayCopy(), - events: $events, - channels: $target['channels'], - roles: $target['roles'], - options: [ - 'projectId' => $projectId, - 'databaseId' => $database->getId(), - 'collectionId' => $collection->getId() - ] - ); - } -} diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php deleted file mode 100644 index 7a20212c9c..0000000000 --- a/src/Appwrite/Platform/Workers/Mails.php +++ /dev/null @@ -1,126 +0,0 @@ -desc('Mails worker') - ->inject('message') - ->inject('register') - ->callback(fn($message, $register) => $this->action($message, $register)); - } - - /** - * @param Message $message - * @param Registry $register - * @throws \PHPMailer\PHPMailer\Exception - * @return void - * @throws Exception - */ - public function action(Message $message, Registry $register): void - { - Runtime::setHookFlags(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP); - $payload = $message->getPayload() ?? []; - - if (empty($payload)) { - throw new Exception('Missing payload'); - } - - $smtp = $payload['smtp']; - - if (empty($smtp) && empty(App::getEnv('_APP_SMTP_HOST'))) { - Console::info('Skipped mail processing. No SMTP configuration has been set.'); - return; - } - - $recipient = $payload['recipient']; - $subject = $payload['subject']; - $variables = $payload['variables']; - $name = $payload['name']; - $body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base.tpl'); - - foreach ($variables as $key => $value) { - $body->setParam('{{' . $key . '}}', $value); - } - - $body = $body->render(); - - /** @var PHPMailer $mail */ - $mail = empty($smtp) - ? $register->get('smtp') - : $this->getMailer($smtp); - - $mail->clearAddresses(); - $mail->clearAllRecipients(); - $mail->clearReplyTos(); - $mail->clearAttachments(); - $mail->clearBCCs(); - $mail->clearCCs(); - $mail->addAddress($recipient, $name); - $mail->Subject = $subject; - $mail->Body = $body; - $mail->AltBody = \strip_tags($body); - - try { - $mail->send(); - } catch (\Exception $error) { - throw new Exception('Error sending mail: ' . $error->getMessage(), 500); - } - } - - /** - * @param array $smtp - * @return PHPMailer - * @throws \PHPMailer\PHPMailer\Exception - */ - protected function getMailer(array $smtp): PHPMailer - { - $mail = new PHPMailer(true); - - $mail->isSMTP(); - - $username = $smtp['username']; - $password = $smtp['password']; - - $mail->XMailer = 'Appwrite Mailer'; - $mail->Host = $smtp['host']; - $mail->Port = $smtp['port']; - $mail->SMTPAuth = (!empty($username) && !empty($password)); - $mail->Username = $username; - $mail->Password = $password; - $mail->SMTPSecure = $smtp['secure']; - $mail->SMTPAutoTLS = false; - $mail->CharSet = 'UTF-8'; - - $mail->setFrom($smtp['senderEmail'], $smtp['senderName']); - - if (!empty($smtp['replyTo'])) { - $mail->addReplyTo($smtp['replyTo'], $smtp['senderName']); - } - - $mail->isHTML(); - - return $mail; - } -} diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php deleted file mode 100644 index 31b0df59a3..0000000000 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ /dev/null @@ -1,330 +0,0 @@ -desc('Migrations worker') - ->inject('message') - ->inject('dbForProject') - ->inject('dbForConsole') - ->callback(fn(Message $message, Database $dbForProject, Database $dbForConsole) => $this->action($message, $dbForProject, $dbForConsole)); - } - - /** - * @param Message $message - * @param Database $dbForProject - * @param Database $dbForConsole - * @return void - * @throws Exception - */ - public function action(Message $message, Database $dbForProject, Database $dbForConsole): void - { - $payload = $message->getPayload() ?? []; - - if (empty($payload)) { - throw new Exception('Missing payload'); - } - - $events = $payload['events'] ?? []; - $project = new Document($payload['project'] ?? []); - $migration = new Document($payload['migration'] ?? []); - - if ($project->getId() === 'console') { - return; - } - - $this->dbForProject = $dbForProject; - $this->dbForConsole = $dbForConsole; - - /** - * Handle Event execution. - */ - if (! empty($events)) { - return; - } - - $this->processMigration($project, $migration); - } - - /** - * @param string $source - * @param array $credentials - * @return Source - * @throws Exception - */ - protected function processSource(string $source, array $credentials): Source - { - return match ($source) { - Firebase::getName() => new Firebase( - json_decode($credentials['serviceAccount'], true), - ), - Supabase::getName() => new Supabase( - $credentials['endpoint'], - $credentials['apiKey'], - $credentials['databaseHost'], - 'postgres', - $credentials['username'], - $credentials['password'], - $credentials['port'], - ), - NHost::getName() => new NHost( - $credentials['subdomain'], - $credentials['region'], - $credentials['adminSecret'], - $credentials['database'], - $credentials['username'], - $credentials['password'], - $credentials['port'], - ), - Appwrite::getName() => new Appwrite($credentials['projectId'], str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], $credentials['apiKey']), - default => throw new \Exception('Invalid source type'), - }; - } - - /** - * @throws Authorization - * @throws Structure - * @throws Conflict - * @throws \Utopia\Database\Exception - * @throws Exception - */ - protected function updateMigrationDocument(Document $migration, Document $project): Document - { - /** Trigger Realtime */ - $allEvents = Event::generateEvents('migrations.[migrationId].update', [ - 'migrationId' => $migration->getId(), - ]); - - $target = Realtime::fromPayload( - event: $allEvents[0], - payload: $migration, - project: $project - ); - - Realtime::send( - projectId: 'console', - payload: $migration->getArrayCopy(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'], - ); - - Realtime::send( - projectId: $project->getId(), - payload: $migration->getArrayCopy(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'], - ); - - return $this->dbForProject->updateDocument('migrations', $migration->getId(), $migration); - } - - /** - * @param Document $apiKey - * @return void - * @throws \Utopia\Database\Exception - * @throws Authorization - * @throws Conflict - * @throws Restricted - * @throws Structure - */ - protected function removeAPIKey(Document $apiKey): void - { - $this->dbForConsole->deleteDocument('keys', $apiKey->getId()); - } - - /** - * @param Document $project - * @return Document - * @throws Authorization - * @throws Structure - * @throws \Utopia\Database\Exception - * @throws Exception - */ - protected function generateAPIKey(Document $project): Document - { - $generatedSecret = bin2hex(\random_bytes(128)); - - $key = new Document([ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'projectInternalId' => $project->getInternalId(), - 'projectId' => $project->getId(), - 'name' => 'Transfer API Key', - 'scopes' => [ - 'users.read', - 'users.write', - 'teams.read', - 'teams.write', - 'databases.read', - 'databases.write', - 'collections.read', - 'collections.write', - 'documents.read', - 'documents.write', - 'buckets.read', - 'buckets.write', - 'files.read', - 'files.write', - 'functions.read', - 'functions.write', - ], - 'expire' => null, - 'sdks' => [], - 'accessedAt' => null, - 'secret' => $generatedSecret, - ]); - - $this->dbForConsole->createDocument('keys', $key); - $this->dbForConsole->deleteCachedDocument('projects', $project->getId()); - - return $key; - } - - /** - * @param Document $project - * @param Document $migration - * @return void - * @throws Authorization - * @throws Conflict - * @throws Restricted - * @throws Structure - * @throws \Utopia\Database\Exception - */ - protected function processMigration(Document $project, Document $migration): void - { - /** - * @var Document $migrationDocument - * @var Transfer $transfer - */ - $migrationDocument = null; - $transfer = null; - $projectDocument = $this->dbForConsole->getDocument('projects', $project->getId()); - $tempAPIKey = $this->generateAPIKey($projectDocument); - - try { - $migrationDocument = $this->dbForProject->getDocument('migrations', $migration->getId()); - $migrationDocument->setAttribute('stage', 'processing'); - $migrationDocument->setAttribute('status', 'processing'); - $this->updateMigrationDocument($migrationDocument, $projectDocument); - - $source = $this->processSource($migrationDocument->getAttribute('source'), $migrationDocument->getAttribute('credentials')); - - $source->report(); - - $destination = new DestinationsAppwrite( - $projectDocument->getId(), - 'http://appwrite/v1', - $tempAPIKey['secret'], - ); - - $transfer = new Transfer( - $source, - $destination - ); - - /** Start Transfer */ - $migrationDocument->setAttribute('stage', 'migrating'); - $this->updateMigrationDocument($migrationDocument, $projectDocument); - $transfer->run($migrationDocument->getAttribute('resources'), function () use ($migrationDocument, $transfer, $projectDocument) { - $migrationDocument->setAttribute('resourceData', json_encode($transfer->getCache())); - $migrationDocument->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); - - $this->updateMigrationDocument($migrationDocument, $projectDocument); - }); - - $errors = $transfer->getReport(Resource::STATUS_ERROR); - - if (count($errors) > 0) { - $migrationDocument->setAttribute('status', 'failed'); - $migrationDocument->setAttribute('stage', 'finished'); - - $errorMessages = []; - foreach ($errors as $error) { - $errorMessages[] = "Failed to transfer resource '{$error['id']}:{$error['resource']}' with message '{$error['message']}'"; - } - - $migrationDocument->setAttribute('errors', $errorMessages); - $this->updateMigrationDocument($migrationDocument, $projectDocument); - - return; - } - - $migrationDocument->setAttribute('status', 'completed'); - $migrationDocument->setAttribute('stage', 'finished'); - } catch (\Throwable $th) { - Console::error($th->getMessage()); - - if ($migrationDocument) { - Console::error($th->getMessage()); - Console::error($th->getTraceAsString()); - $migrationDocument->setAttribute('status', 'failed'); - $migrationDocument->setAttribute('stage', 'finished'); - $migrationDocument->setAttribute('errors', [$th->getMessage()]); - - return; - } - - if ($transfer) { - $errors = $transfer->getReport(Resource::STATUS_ERROR); - - if (count($errors) > 0) { - $migrationDocument->setAttribute('status', 'failed'); - $migrationDocument->setAttribute('stage', 'finished'); - $migrationDocument->setAttribute('errors', $errors); - } - } - } finally { - if ($migrationDocument) { - $this->updateMigrationDocument($migrationDocument, $projectDocument); - } - if ($tempAPIKey) { - $this->removeAPIKey($tempAPIKey); - } - } - } -} diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php deleted file mode 100644 index dd7b92bf5e..0000000000 --- a/src/Appwrite/Platform/Workers/Webhooks.php +++ /dev/null @@ -1,118 +0,0 @@ -desc('Webhooks worker') - ->inject('message') - ->callback(fn($message) => $this->action($message)); - } - - /** - * @param Message $message - * @return void - * @throws Exception - */ - public function action(Message $message): void - { - $payload = $message->getPayload() ?? []; - - if (empty($payload)) { - throw new Exception('Missing payload'); - } - - $events = $payload['events']; - $webhookPayload = json_encode($payload['payload']); - $project = new Document($payload['project']); - $user = new Document($payload['user'] ?? []); - - foreach ($project->getAttribute('webhooks', []) as $webhook) { - if (array_intersect($webhook->getAttribute('events', []), $events)) { - $this->execute($events, $webhookPayload, $webhook, $user, $project); - } - } - - if (!empty($this->errors)) { - throw new Exception(\implode(" / \n\n", $this->errors)); - } - } - - /** - * @param array $events - * @param string $payload - * @param Document $webhook - * @param Document $user - * @param Document $project - * @return void - */ - private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project): void - { - - $url = \rawurldecode($webhook->getAttribute('url')); - $signatureKey = $webhook->getAttribute('signatureKey'); - $signature = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true)); - $httpUser = $webhook->getAttribute('httpUser'); - $httpPass = $webhook->getAttribute('httpPass'); - $ch = \curl_init($webhook->getAttribute('url')); - - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); - \curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); - \curl_setopt($ch, CURLOPT_HEADER, 0); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_USERAGENT, \sprintf( - APP_USERAGENT, - App::getEnv('_APP_VERSION', 'UNKNOWN'), - App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) - )); - \curl_setopt( - $ch, - CURLOPT_HTTPHEADER, - [ - 'Content-Type: application/json', - 'Content-Length: ' . \strlen($payload), - 'X-' . APP_NAME . '-Webhook-Id: ' . $webhook->getId(), - 'X-' . APP_NAME . '-Webhook-Events: ' . implode(',', $events), - 'X-' . APP_NAME . '-Webhook-Name: ' . $webhook->getAttribute('name', ''), - 'X-' . APP_NAME . '-Webhook-User-Id: ' . $user->getId(), - 'X-' . APP_NAME . '-Webhook-Project-Id: ' . $project->getId(), - 'X-' . APP_NAME . '-Webhook-Signature: ' . $signature, - ] - ); - - if (!$webhook->getAttribute('security', true)) { - \curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - \curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - } - - if (!empty($httpUser) && !empty($httpPass)) { - \curl_setopt($ch, CURLOPT_USERPWD, "$httpUser:$httpPass"); - \curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - } - - if (false === \curl_exec($ch)) { - $this->errors[] = \curl_error($ch) . ' in events ' . implode(', ', $events) . ' for webhook ' . $webhook->getAttribute('name'); - } - - \curl_close($ch); - } -} From 025a0292f4b61850c67a97e12305f9d3dfedf3a6 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 21 Oct 2023 07:02:44 -0400 Subject: [PATCH 03/61] Revert "Sync with main" This reverts commit 0ea1418132b194c173e6efe211042e985c73b075. --- .../examples/health/get-queue-builds.md | 18 + .../examples/health/get-queue-databases.md | 18 + .../examples/health/get-queue-deletes.md | 18 + .../examples/health/get-queue-mails.md | 18 + .../Platform/Workers/Certificates.php | 534 +++++++++++++++ src/Appwrite/Platform/Workers/Databases.php | 622 ++++++++++++++++++ src/Appwrite/Platform/Workers/Mails.php | 126 ++++ src/Appwrite/Platform/Workers/Migrations.php | 330 ++++++++++ src/Appwrite/Platform/Workers/Webhooks.php | 118 ++++ 9 files changed, 1802 insertions(+) create mode 100644 docs/examples/1.4.x/console-web/examples/health/get-queue-builds.md create mode 100644 docs/examples/1.4.x/console-web/examples/health/get-queue-databases.md create mode 100644 docs/examples/1.4.x/console-web/examples/health/get-queue-deletes.md create mode 100644 docs/examples/1.4.x/console-web/examples/health/get-queue-mails.md create mode 100644 src/Appwrite/Platform/Workers/Certificates.php create mode 100644 src/Appwrite/Platform/Workers/Databases.php create mode 100644 src/Appwrite/Platform/Workers/Mails.php create mode 100644 src/Appwrite/Platform/Workers/Migrations.php create mode 100644 src/Appwrite/Platform/Workers/Webhooks.php diff --git a/docs/examples/1.4.x/console-web/examples/health/get-queue-builds.md b/docs/examples/1.4.x/console-web/examples/health/get-queue-builds.md new file mode 100644 index 0000000000..a312fa7df0 --- /dev/null +++ b/docs/examples/1.4.x/console-web/examples/health/get-queue-builds.md @@ -0,0 +1,18 @@ +import { Client, Health } from "@appwrite.io/console"; + +const client = new Client(); + +const health = new Health(client); + +client + .setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID +; + +const promise = health.getQueueBuilds(); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/docs/examples/1.4.x/console-web/examples/health/get-queue-databases.md b/docs/examples/1.4.x/console-web/examples/health/get-queue-databases.md new file mode 100644 index 0000000000..481480a80d --- /dev/null +++ b/docs/examples/1.4.x/console-web/examples/health/get-queue-databases.md @@ -0,0 +1,18 @@ +import { Client, Health } from "@appwrite.io/console"; + +const client = new Client(); + +const health = new Health(client); + +client + .setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID +; + +const promise = health.getQueueDatabases(); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/docs/examples/1.4.x/console-web/examples/health/get-queue-deletes.md b/docs/examples/1.4.x/console-web/examples/health/get-queue-deletes.md new file mode 100644 index 0000000000..c1bbb9f655 --- /dev/null +++ b/docs/examples/1.4.x/console-web/examples/health/get-queue-deletes.md @@ -0,0 +1,18 @@ +import { Client, Health } from "@appwrite.io/console"; + +const client = new Client(); + +const health = new Health(client); + +client + .setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID +; + +const promise = health.getQueueDeletes(); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/docs/examples/1.4.x/console-web/examples/health/get-queue-mails.md b/docs/examples/1.4.x/console-web/examples/health/get-queue-mails.md new file mode 100644 index 0000000000..8d6d68228a --- /dev/null +++ b/docs/examples/1.4.x/console-web/examples/health/get-queue-mails.md @@ -0,0 +1,18 @@ +import { Client, Health } from "@appwrite.io/console"; + +const client = new Client(); + +const health = new Health(client); + +client + .setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID +; + +const promise = health.getQueueMails(); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php new file mode 100644 index 0000000000..02c1835dd5 --- /dev/null +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -0,0 +1,534 @@ +desc('Certificates worker') + ->inject('message') + ->inject('dbForConsole') + ->inject('queueForMails') + ->inject('queueForEvents') + ->inject('queueForFunctions') + ->callback(fn(Message $message, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions) => $this->action($message, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions)); + } + + /** + * @param Message $message + * @param Database $dbForConsole + * @param Mail $queueForMails + * @param Event $queueForEvents + * @param Func $queueForFunctions + * @return void + * @throws Throwable + * @throws \Utopia\Database\Exception + */ + public function action(Message $message, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions): void + { + $payload = $message->getPayload() ?? []; + + if (empty($payload)) { + throw new Exception('Missing payload'); + } + + $document = new Document($payload['domain'] ?? []); + $domain = new Domain($document->getAttribute('domain', '')); + $skipRenewCheck = $payload['skipRenewCheck'] ?? false; + + $this->execute($domain, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $skipRenewCheck); + } + + /** + * @param Domain $domain + * @param Database $dbForConsole + * @param Mail $queueForMails + * @param Event $queueForEvents + * @param Func $queueForFunctions + * @param bool $skipRenewCheck + * @return void + * @throws Throwable + * @throws \Utopia\Database\Exception + */ + private function execute(Domain $domain, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, bool $skipRenewCheck = false): void + { + /** + * 1. Read arguments and validate domain + * 2. Get main domain + * 3. Validate CNAME DNS if parameter is not main domain (meaning it's custom domain) + * 4. Validate security email. Cannot be empty, required by LetsEncrypt + * 5. Validate renew date with certificate file, unless requested to skip by parameter + * 6. Issue a certificate using certbot CLI + * 7. Update 'log' attribute on certificate document with Certbot message + * 8. Create storage folder for certificate, if not ready already + * 9. Move certificates from Certbot location to our Storage + * 10. Create/Update our Storage with new Traefik config with new certificate paths + * 11. Read certificate file and update 'renewDate' on certificate document + * 12. Update 'issueDate' and 'attempts' on certificate + * + * If at any point unexpected error occurs, program stops without applying changes to document, and error is thrown into worker + * + * If code stops with expected error: + * 1. 'log' attribute on document is updated with error message + * 2. 'attempts' amount is increased + * 3. Console log is shown + * 4. Email is sent to security email + * + * Unless unexpected error occurs, at the end, we: + * 1. Update 'updated' attribute on document + * 2. Save document to database + * 3. Update all domains documents with current certificate ID + * + * Note: Renewals are checked and scheduled from maintenence worker + */ + + // Get current certificate + $certificate = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain->get()])]); + + // If we don't have certificate for domain yet, let's create new document. At the end we save it + if (!$certificate) { + $certificate = new Document(); + $certificate->setAttribute('domain', $domain->get()); + } + + $success = false; + + try { + // Email for alerts is required by LetsEncrypt + $email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'); + if (empty($email)) { + throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate.'); + } + + // Validate domain and DNS records. Skip if job is forced + if (!$skipRenewCheck) { + $mainDomain = $this->getMainDomain(); + $isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain; + $this->validateDomain($domain, $isMainDomain); + } + + // If certificate exists already, double-check expiry date. Skip if job is forced + if (!$skipRenewCheck && !$this->isRenewRequired($domain->get())) { + throw new Exception('Renew isn\'t required.'); + } + + // Prepare folder name for certbot. Using this helps prevent miss-match in LetsEncrypt configuration when renewing certificate + $folder = ID::unique(); + + // Generate certificate files using Let's Encrypt + $letsEncryptData = $this->issueCertificate($folder, $domain->get(), $email); + + // Command succeeded, store all data into document + $logs = 'Certificate successfully generated.'; + $certificate->setAttribute('logs', \mb_strcut($logs, 0, 1000000));// Limit to 1MB + + + // Give certificates to Traefik + $this->applyCertificateFiles($folder, $domain->get(), $letsEncryptData); + + // Update certificate info stored in database + $certificate->setAttribute('renewDate', $this->getRenewDate($domain->get())); + $certificate->setAttribute('attempts', 0); + $certificate->setAttribute('issueDate', DateTime::now()); + $success = true; + } catch (Throwable $e) { + $logs = $e->getMessage(); + + // Set exception as log in certificate document + $certificate->setAttribute('logs', \mb_strcut($logs, 0, 1000000));// Limit to 1MB + + // Increase attempts count + $attempts = $certificate->getAttribute('attempts', 0) + 1; + $certificate->setAttribute('attempts', $attempts); + + // Store cuttent time as renew date to ensure another attempt in next maintenance cycle + $certificate->setAttribute('renewDate', DateTime::now()); + + // Send email to security email + $this->notifyError($domain->get(), $e->getMessage(), $attempts, $queueForMails); + } finally { + // All actions result in new updatedAt date + $certificate->setAttribute('updated', DateTime::now()); + + // Save all changes we made to certificate document into database + $this->saveCertificateDocument($domain->get(), $certificate, $success, $dbForConsole, $queueForEvents, $queueForFunctions); + } + } + + /** + * Save certificate data into database. + * + * @param string $domain Domain name that certificate is for + * @param Document $certificate Certificate document that we need to save + * @param bool $success + * @param Database $dbForConsole Database connection for console + * @param Event $queueForEvents + * @param Func $queueForFunctions + * @return void + * @throws \Utopia\Database\Exception + * @throws Authorization + * @throws Conflict + * @throws Structure + */ + private function saveCertificateDocument(string $domain, Document $certificate, bool $success, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions): void + { + // Check if update or insert required + $certificateDocument = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain])]); + if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) { + // Merge new data with current data + $certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy())); + $certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate); + } else { + $certificate->removeAttribute('$internalId'); + $certificate = $dbForConsole->createDocument('certificates', $certificate); + } + + $certificateId = $certificate->getId(); + $this->updateDomainDocuments($certificateId, $domain, $success, $dbForConsole, $queueForEvents, $queueForFunctions); + } + + /** + * Get main domain. Needed as we do different checks for main and non-main domains. + * + * @return null|string Returns main domain. If null, there is no main domain yet. + */ + private function getMainDomain(): ?string + { + $envDomain = App::getEnv('_APP_DOMAIN', ''); + if (!empty($envDomain) && $envDomain !== 'localhost') { + return $envDomain; + } + + return null; + } + + /** + * Internal domain validation functionality to prevent unnecessary attempts failed from Let's Encrypt side. We check: + * - Domain needs to be public and valid (prevents NFT domains that are not supported by Let's Encrypt) + * - Domain must have proper DNS record + * + * @param Domain $domain Domain which we validate + * @param bool $isMainDomain In case of master domain, we look for different DNS configurations + * + * @return void + * @throws Exception + */ + private function validateDomain(Domain $domain, bool $isMainDomain): void + { + if (empty($domain->get())) { + throw new Exception('Missing certificate domain.'); + } + + if (!$domain->isKnown() || $domain->isTest()) { + throw new Exception('Unknown public suffix for domain.'); + } + + if (!$isMainDomain) { + // TODO: Would be awesome to also support A/AAAA records here. Maybe dry run? + // Validate if domain target is properly configured + $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); + + if (!$target->isKnown() || $target->isTest()) { + throw new Exception('Unreachable CNAME target (' . $target->get() . '), please use a domain with a public suffix.'); + } + + // Verify domain with DNS records + $validator = new CNAME($target->get()); + if (!$validator->isValid($domain->get())) { + throw new Exception('Failed to verify domain DNS records.'); + } + } else { + // Main domain validation + // TODO: Would be awesome to check A/AAAA record here. Maybe dry run? + } + } + + /** + * Reads expiry date of certificate from file and decides if renewal is required or not. + * + * @param string $domain Domain for which we check certificate file + * @return bool True, if certificate needs to be renewed + * @throws Exception + */ + private function isRenewRequired(string $domain): bool + { + $certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem'; + if (\file_exists($certPath)) { + $validTo = null; + + $certData = openssl_x509_parse(file_get_contents($certPath)); + $validTo = $certData['validTo_time_t'] ?? 0; + + if (empty($validTo)) { + throw new Exception('Unable to read certificate file (cert.pem).'); + } + + // LetsEncrypt allows renewal 30 days before expiry + $expiryInAdvance = (60 * 60 * 24 * 30); + if ($validTo - $expiryInAdvance > \time()) { + return false; + } + } + + return true; + } + + /** + * LetsEncrypt communication to issue certificate (using certbot CLI) + * + * @param string $folder Folder into which certificates should be generated + * @param string $domain Domain to generate certificate for + * @return array Named array with keys 'stdout' and 'stderr', both string + * @throws Exception + */ + private function issueCertificate(string $folder, string $domain, string $email): array + { + $stdout = ''; + $stderr = ''; + + $staging = (App::isProduction()) ? '' : ' --dry-run'; + $exit = Console::execute("certbot certonly -v --webroot --noninteractive --agree-tos{$staging}" + . " --email " . $email + . " --cert-name " . $folder + . " -w " . APP_STORAGE_CERTIFICATES + . " -d {$domain}", '', $stdout, $stderr); + + // Unexpected error, usually 5XX, API limits, ... + if ($exit !== 0) { + throw new Exception('Failed to issue a certificate with message: ' . $stderr); + } + + return [ + 'stdout' => $stdout, + 'stderr' => $stderr + ]; + } + + /** + * Read new renew date from certificate file generated by Let's Encrypt + * + * @param string $domain Domain which certificate was generated for + * @return string + * @throws \Utopia\Database\Exception + */ + private function getRenewDate(string $domain): string + { + $certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem'; + $certData = openssl_x509_parse(file_get_contents($certPath)); + $validTo = $certData['validTo_time_t'] ?? null; + $dt = (new \DateTime())->setTimestamp($validTo); + return DateTime::addSeconds($dt, -60 * 60 * 24 * 30); // -30 days + } + + /** + * Method to take files from Let's Encrypt, and put it into Traefik. + * + * @param string $domain Domain which certificate was generated for + * @param string $folder Folder in which certificates were generated + * @param array $letsEncryptData Let's Encrypt logs to use for additional info when throwing error + * @return void + * @throws Exception + */ + private function applyCertificateFiles(string $folder, string $domain, array $letsEncryptData): void + { + + // Prepare folder in storage for domain + $path = APP_STORAGE_CERTIFICATES . '/' . $domain; + if (!\is_readable($path)) { + if (!\mkdir($path, 0755, true)) { + throw new Exception('Failed to create path for certificate.'); + } + } + + // Move generated files + if (!@\rename('/etc/letsencrypt/live/' . $folder . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) { + throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + } + + if (!@\rename('/etc/letsencrypt/live/' . $folder . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) { + throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + } + + if (!@\rename('/etc/letsencrypt/live/' . $folder . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) { + throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + } + + if (!@\rename('/etc/letsencrypt/live/' . $folder . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) { + throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + } + + $config = \implode(PHP_EOL, [ + "tls:", + " certificates:", + " - certFile: /storage/certificates/{$domain}/fullchain.pem", + " keyFile: /storage/certificates/{$domain}/privkey.pem" + ]); + + // Save configuration into Traefik using our new cert files + if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain . '.yml', $config)) { + throw new Exception('Failed to save Traefik configuration.'); + } + } + + /** + * Method to make sure information about error is delivered to admnistrator. + * + * @param string $domain Domain that caused the error + * @param string $errorMessage Verbose error message + * @param int $attempt How many times it failed already + * @param Mail $queueForMails + * @return void + * @throws Exception + */ + private function notifyError(string $domain, string $errorMessage, int $attempt, Mail $queueForMails): void + { + // Log error into console + Console::warning('Cannot renew domain (' . $domain . ') on attempt no. ' . $attempt . ' certificate: ' . $errorMessage); + + // Send mail to administratore mail + + $locale = new Locale(App::getEnv('_APP_LOCALE', 'en')); + if (!$locale->getText('emails.sender') || !$locale->getText("emails.certificate.hello") || !$locale->getText("emails.certificate.subject") || !$locale->getText("emails.certificate.body") || !$locale->getText("emails.certificate.footer") || !$locale->getText("emails.certificate.thanks") || !$locale->getText("emails.certificate.signature")) { + $locale->setDefault('en'); + } + + $body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base.tpl'); + + $subject = \sprintf($locale->getText("emails.certificate.subject"), $domain); + $body + ->setParam('{{domain}}', $domain) + ->setParam('{{error}}', $errorMessage) + ->setParam('{{attempt}}', $attempt) + ->setParam('{{subject}}', $subject) + ->setParam('{{hello}}', $locale->getText("emails.certificate.hello")) + ->setParam('{{body}}', $locale->getText("emails.certificate.body")) + ->setParam('{{redirect}}', 'https://' . $domain) + ->setParam('{{footer}}', $locale->getText("emails.certificate.footer")) + ->setParam('{{thanks}}', $locale->getText("emails.certificate.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.certificate.signature")) + ->setParam('{{project}}', 'Console') + ->setParam('{{direction}}', $locale->getText('settings.direction')) + ->setParam('{{bg-body}}', '#f7f7f7') + ->setParam('{{bg-content}}', '#ffffff') + ->setParam('{{text-content}}', '#000000'); + + $queueForMails + ->setRecipient(App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')) + ->setBody($body->render()) + ->setName('Appwrite Administrator') + ->trigger(); + } + + /** + * Update all existing domain documents so they have relation to correct certificate document. + * This solved issues: + * - when adding a domain for which there is already a certificate + * - when renew creates new document? It might? + * - overall makes it more reliable + * + * @param string $certificateId ID of a new or updated certificate document + * @param string $domain Domain that is affected by new certificate + * @param bool $success Was certificate generation successful? + * + * @return void + */ + private function updateDomainDocuments(string $certificateId, string $domain, bool $success, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions): void + { + + $rule = $dbForConsole->findOne('rules', [ + Query::equal('domain', [$domain]), + ]); + + if ($rule !== false && !$rule->isEmpty()) { + $rule->setAttribute('certificateId', $certificateId); + $rule->setAttribute('status', $success ? 'verified' : 'unverified'); + $dbForConsole->updateDocument('rules', $rule->getId(), $rule); + + $projectId = $rule->getAttribute('projectId'); + + // Skip events for console project (triggered by auto-ssl generation for 1 click setups) + if ($projectId === 'console') { + return; + } + + $project = $dbForConsole->getDocument('projects', $projectId); + + /** Trigger Webhook */ + $ruleModel = new Rule(); + $queueForEvents + ->setProject($project) + ->setEvent('rules.[ruleId].update') + ->setParam('ruleId', $rule->getId()) + ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) + ->trigger(); + + + /** Trigger Functions */ + $queueForFunctions + ->setProject($project) + ->setEvent('rules.[ruleId].update') + ->setParam('ruleId', $rule->getId()) + ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) + ->trigger(); + + /** Trigger realtime event */ + $allEvents = Event::generateEvents('rules.[ruleId].update', [ + 'ruleId' => $rule->getId(), + ]); + $target = Realtime::fromPayload( + // Pass first, most verbose event pattern + event: $allEvents[0], + payload: $rule, + project: $project + ); + Realtime::send( + projectId: 'console', + payload: $rule->getArrayCopy(), + events: $allEvents, + channels: $target['channels'], + roles: $target['roles'] + ); + Realtime::send( + projectId: $project->getId(), + payload: $rule->getArrayCopy(), + events: $allEvents, + channels: $target['channels'], + roles: $target['roles'] + ); + } + } +} diff --git a/src/Appwrite/Platform/Workers/Databases.php b/src/Appwrite/Platform/Workers/Databases.php new file mode 100644 index 0000000000..e0ec75e1d4 --- /dev/null +++ b/src/Appwrite/Platform/Workers/Databases.php @@ -0,0 +1,622 @@ +desc('Databases worker') + ->inject('message') + ->inject('dbForConsole') + ->inject('dbForProject') + ->callback(fn($message, $dbForConsole, $dbForProject) => $this->action($message, $dbForConsole, $dbForProject)); + } + + /** + * @param Message $message + * @param Database $dbForConsole + * @param Database $dbForProject + * @return void + * @throws \Exception + */ + public function action(Message $message, Database $dbForConsole, Database $dbForProject): void + { + $payload = $message->getPayload() ?? []; + + if (empty($payload)) { + throw new \Exception('Missing payload'); + } + + $type = $payload['type']; + $project = new Document($payload['project']); + $collection = new Document($payload['collection'] ?? []); + $document = new Document($payload['document'] ?? []); + $database = new Document($payload['database'] ?? []); + + if ($database->isEmpty()) { + throw new Exception('Missing database'); + } + + match (strval($type)) { + DATABASE_TYPE_DELETE_DATABASE => $this->deleteDatabase($database, $project, $dbForProject), + DATABASE_TYPE_DELETE_COLLECTION => $this->deleteCollection($database, $collection, $project, $dbForProject), + DATABASE_TYPE_CREATE_ATTRIBUTE => $this->createAttribute($database, $collection, $document, $project, $dbForConsole, $dbForProject), + DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForConsole, $dbForProject), + DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForConsole, $dbForProject), + DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForConsole, $dbForProject), + default => Console::error('No database operation for type: ' . $type), + }; + } + + /** + * @param Document $database + * @param Document $collection + * @param Document $attribute + * @param Document $project + * @param Database $dbForConsole + * @param Database $dbForProject + * @return void + * @throws Authorization + * @throws Conflict + * @throws \Exception + */ + private function createAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForConsole, Database $dbForProject): void + { + if ($collection->isEmpty()) { + throw new Exception('Missing collection'); + } + if ($attribute->isEmpty()) { + throw new Exception('Missing attribute'); + } + + $projectId = $project->getId(); + + $events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].update', [ + 'databaseId' => $database->getId(), + 'collectionId' => $collection->getId(), + 'attributeId' => $attribute->getId() + ]); + /** + * TODO @christyjacob4 verify if this is still the case + * Fetch attribute from the database, since with Resque float values are loosing informations. + */ + $attribute = $dbForProject->getDocument('attributes', $attribute->getId()); + + $collectionId = $collection->getId(); + $key = $attribute->getAttribute('key', ''); + $type = $attribute->getAttribute('type', ''); + $size = $attribute->getAttribute('size', 0); + $required = $attribute->getAttribute('required', false); + $default = $attribute->getAttribute('default', null); + $signed = $attribute->getAttribute('signed', true); + $array = $attribute->getAttribute('array', false); + $format = $attribute->getAttribute('format', ''); + $formatOptions = $attribute->getAttribute('formatOptions', []); + $filters = $attribute->getAttribute('filters', []); + $options = $attribute->getAttribute('options', []); + $project = $dbForConsole->getDocument('projects', $projectId); + + + try { + switch ($type) { + case Database::VAR_RELATIONSHIP: + $relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']); + if ($relatedCollection->isEmpty()) { + throw new DatabaseException('Collection not found'); + } + + if ( + !$dbForProject->createRelationship( + collection: 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), + relatedCollection: 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), + type: $options['relationType'], + twoWay: $options['twoWay'], + id: $key, + twoWayKey: $options['twoWayKey'], + onDelete: $options['onDelete'], + ) + ) { + throw new DatabaseException('Failed to create Attribute'); + } + + if ($options['twoWay']) { + $relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']); + $dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'available')); + } + break; + default: + if (!$dbForProject->createAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters)) { + throw new \Exception('Failed to create Attribute'); + } + } + + $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available')); + } catch (\Exception $e) { + Console::error($e->getMessage()); + + if ($e instanceof DatabaseException) { + $attribute->setAttribute('error', $e->getMessage()); + if (isset($relatedAttribute)) { + $relatedAttribute->setAttribute('error', $e->getMessage()); + } + } + + $dbForProject->updateDocument( + 'attributes', + $attribute->getId(), + $attribute->setAttribute('status', 'failed') + ); + + if (isset($relatedAttribute)) { + $dbForProject->updateDocument( + 'attributes', + $relatedAttribute->getId(), + $relatedAttribute->setAttribute('status', 'failed') + ); + } + } finally { + $this->trigger($database, $collection, $attribute, $project, $projectId, $events); + } + + if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) { + $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId()); + } + + $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId); + } + + /** + * @param Document $database + * @param Document $collection + * @param Document $attribute + * @param Document $project + * @param Database $dbForConsole + * @param Database $dbForProject + * @return void + * @throws Authorization + * @throws Conflict + * @throws \Exception + **/ + private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForConsole, Database $dbForProject): void + { + if ($collection->isEmpty()) { + throw new Exception('Missing collection'); + } + if ($attribute->isEmpty()) { + throw new Exception('Missing attribute'); + } + + $projectId = $project->getId(); + + $events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete', [ + 'databaseId' => $database->getId(), + 'collectionId' => $collection->getId(), + 'attributeId' => $attribute->getId() + ]); + $collectionId = $collection->getId(); + $key = $attribute->getAttribute('key', ''); + $status = $attribute->getAttribute('status', ''); + $type = $attribute->getAttribute('type', ''); + $project = $dbForConsole->getDocument('projects', $projectId); + $options = $attribute->getAttribute('options', []); + $relatedAttribute = new Document(); + $relatedCollection = new Document(); + // possible states at this point: + // - available: should not land in queue; controller flips these to 'deleting' + // - processing: hasn't finished creating + // - deleting: was available, in deletion queue for first time + // - failed: attribute was never created + // - stuck: attribute was available but cannot be removed + + try { + if ($status !== 'failed') { + if ($type === Database::VAR_RELATIONSHIP) { + if ($options['twoWay']) { + $relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']); + if ($relatedCollection->isEmpty()) { + throw new DatabaseException('Collection not found'); + } + $relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']); + } + + if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) { + $dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck')); + throw new DatabaseException('Failed to delete Relationship'); + } + } elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) { + throw new DatabaseException('Failed to delete Attribute'); + } + } + + $dbForProject->deleteDocument('attributes', $attribute->getId()); + + if (!$relatedAttribute->isEmpty()) { + $dbForProject->deleteDocument('attributes', $relatedAttribute->getId()); + } + } catch (\Exception $e) { + Console::error($e->getMessage()); + + if ($e instanceof DatabaseException) { + $attribute->setAttribute('error', $e->getMessage()); + if (!$relatedAttribute->isEmpty()) { + $relatedAttribute->setAttribute('error', $e->getMessage()); + } + } + $dbForProject->updateDocument( + 'attributes', + $attribute->getId(), + $attribute->setAttribute('status', 'stuck') + ); + if (!$relatedAttribute->isEmpty()) { + $dbForProject->updateDocument( + 'attributes', + $relatedAttribute->getId(), + $relatedAttribute->setAttribute('status', 'stuck') + ); + } + } finally { + $this->trigger($database, $collection, $attribute, $project, $projectId, $events); + } + + // The underlying database removes/rebuilds indexes when attribute is removed + // Update indexes table with changes + /** @var Document[] $indexes */ + $indexes = $collection->getAttribute('indexes', []); + + foreach ($indexes as $index) { + /** @var string[] $attributes */ + $attributes = $index->getAttribute('attributes'); + $lengths = $index->getAttribute('lengths'); + $orders = $index->getAttribute('orders'); + + $found = \array_search($key, $attributes); + + if ($found !== false) { + // If found, remove entry from attributes, lengths, and orders + // array_values wraps array_diff to reindex array keys + // when found attribute is removed from array + $attributes = \array_values(\array_diff($attributes, [$attributes[$found]])); + $lengths = \array_values(\array_diff($lengths, isset($lengths[$found]) ? [$lengths[$found]] : [])); + $orders = \array_values(\array_diff($orders, isset($orders[$found]) ? [$orders[$found]] : [])); + + if (empty($attributes)) { + $dbForProject->deleteDocument('indexes', $index->getId()); + } else { + $index + ->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN) + ->setAttribute('lengths', $lengths, Document::SET_TYPE_ASSIGN) + ->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN); + + // Check if an index exists with the same attributes and orders + $exists = false; + foreach ($indexes as $existing) { + if ( + $existing->getAttribute('key') !== $index->getAttribute('key') // Ignore itself + && $existing->getAttribute('attributes') === $index->getAttribute('attributes') + && $existing->getAttribute('orders') === $index->getAttribute('orders') + ) { + $exists = true; + break; + } + } + + if ($exists) { // Delete the duplicate if created, else update in db + $this->deleteIndex($database, $collection, $index, $project, $dbForConsole, $dbForProject); + } else { + $dbForProject->updateDocument('indexes', $index->getId(), $index); + } + } + } + } + + $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId); + $dbForProject->deleteCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); + + if (!$relatedCollection->isEmpty() && !$relatedAttribute->isEmpty()) { + $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId()); + $dbForProject->deleteCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); + } + } + + /** + * @param Document $database + * @param Document $collection + * @param Document $index + * @param Document $project + * @param Database $dbForConsole + * @param Database $dbForProject + * @return void + * @throws Authorization + * @throws Conflict + * @throws Structure + * @throws DatabaseException + */ + private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForConsole, Database $dbForProject): void + { + if ($collection->isEmpty()) { + throw new Exception('Missing collection'); + } + if ($index->isEmpty()) { + throw new Exception('Missing index'); + } + + $projectId = $project->getId(); + + $events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].update', [ + 'databaseId' => $database->getId(), + 'collectionId' => $collection->getId(), + 'indexId' => $index->getId() + ]); + $collectionId = $collection->getId(); + $key = $index->getAttribute('key', ''); + $type = $index->getAttribute('type', ''); + $attributes = $index->getAttribute('attributes', []); + $lengths = $index->getAttribute('lengths', []); + $orders = $index->getAttribute('orders', []); + $project = $dbForConsole->getDocument('projects', $projectId); + + try { + if (!$dbForProject->createIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $attributes, $lengths, $orders)) { + throw new DatabaseException('Failed to create Index'); + } + $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available')); + } catch (\Exception $e) { + Console::error($e->getMessage()); + + if ($e instanceof DatabaseException) { + $index->setAttribute('error', $e->getMessage()); + } + $dbForProject->updateDocument( + 'indexes', + $index->getId(), + $index->setAttribute('status', 'failed') + ); + } finally { + $this->trigger($database, $collection, $index, $project, $projectId, $events); + } + + $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId); + } + + /** + * @param Document $database + * @param Document $collection + * @param Document $index + * @param Document $project + * @param Database $dbForConsole + * @param Database $dbForProject + * @return void + * @throws Authorization + * @throws Conflict + * @throws Structure + * @throws DatabaseException + */ + private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForConsole, Database $dbForProject): void + { + if ($collection->isEmpty()) { + throw new Exception('Missing collection'); + } + if ($index->isEmpty()) { + throw new Exception('Missing index'); + } + + $projectId = $project->getId(); + + $events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].delete', [ + 'databaseId' => $database->getId(), + 'collectionId' => $collection->getId(), + 'indexId' => $index->getId() + ]); + $key = $index->getAttribute('key'); + $status = $index->getAttribute('status', ''); + $project = $dbForConsole->getDocument('projects', $projectId); + + try { + if ($status !== 'failed' && !$dbForProject->deleteIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) { + throw new DatabaseException('Failed to delete index'); + } + $dbForProject->deleteDocument('indexes', $index->getId()); + $index->setAttribute('status', 'deleted'); + } catch (\Exception $e) { + Console::error($e->getMessage()); + + if ($e instanceof DatabaseException) { + $index->setAttribute('error', $e->getMessage()); + } + $dbForProject->updateDocument( + 'indexes', + $index->getId(), + $index->setAttribute('status', 'stuck') + ); + } finally { + $this->trigger($database, $collection, $index, $project, $projectId, $events); + } + + $dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collection->getId()); + } + + /** + * @param Document $database + * @param Document $project + * @param $dbForProject + * @return void + * @throws Exception + */ + protected function deleteDatabase(Document $database, Document $project, $dbForProject): void + { + $this->deleteByGroup('database_' . $database->getInternalId(), [], $dbForProject, function ($collection) use ($database, $project, $dbForProject) { + $this->deleteCollection($database, $collection, $project, $dbForProject); + }); + + $dbForProject->deleteCollection('database_' . $database->getInternalId()); + + $this->deleteAuditLogsByResource('database/' . $database->getId(), $project, $dbForProject); + } + + /** + * @param Document $database + * @param Document $collection + * @param Document $project + * @param Database $dbForProject + * @return void + * @throws Authorization + * @throws Conflict + * @throws DatabaseException + * @throws Restricted + * @throws Structure + */ + protected function deleteCollection(Document $database, Document $collection, Document $project, Database $dbForProject): void + { + if ($collection->isEmpty()) { + throw new Exception('Missing collection'); + } + + $collectionId = $collection->getId(); + $collectionInternalId = $collection->getInternalId(); + $databaseId = $database->getId(); + $databaseInternalId = $database->getInternalId(); + + $relationships = \array_filter( + $collection->getAttribute('attributes'), + fn ($attribute) => $attribute['type'] === Database::VAR_RELATIONSHIP + ); + + foreach ($relationships as $relationship) { + if (!$relationship['twoWay']) { + continue; + } + $relatedCollection = $dbForProject->getDocument('database_' . $databaseInternalId, $relationship['relatedCollection']); + $dbForProject->deleteDocument('attributes', $databaseInternalId . '_' . $relatedCollection->getInternalId() . '_' . $relationship['twoWayKey']); + $dbForProject->deleteCachedDocument('database_' . $databaseInternalId, $relatedCollection->getId()); + $dbForProject->deleteCachedCollection('database_' . $databaseInternalId . '_collection_' . $relatedCollection->getInternalId()); + } + + $dbForProject->deleteCollection('database_' . $databaseInternalId . '_collection_' . $collection->getInternalId()); + + $this->deleteByGroup('attributes', [ + Query::equal('databaseInternalId', [$databaseInternalId]), + Query::equal('collectionInternalId', [$collectionInternalId]) + ], $dbForProject); + + $this->deleteByGroup('indexes', [ + Query::equal('databaseInternalId', [$databaseInternalId]), + Query::equal('collectionInternalId', [$collectionInternalId]) + ], $dbForProject); + + $this->deleteAuditLogsByResource('database/' . $databaseId . '/collection/' . $collectionId, $project, $dbForProject); + } + + /** + * @param string $resource + * @param Document $project + * @param Database $dbForProject + * @return void + * @throws Exception + */ + protected function deleteAuditLogsByResource(string $resource, Document $project, Database $dbForProject): void + { + $this->deleteByGroup(Audit::COLLECTION, [ + Query::equal('resource', [$resource]) + ], $dbForProject); + } + + /** + * @param string $collection collectionID + * @param array $queries + * @param Database $database + * @param callable|null $callback + * @return void + * @throws Exception + */ + protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null): void + { + $count = 0; + $chunk = 0; + $limit = 50; + $sum = $limit; + + $executionStart = \microtime(true); + + while ($sum === $limit) { + $chunk++; + + $results = $database->find($collection, \array_merge([Query::limit($limit)], $queries)); + + $sum = count($results); + + Console::info('Deleting chunk #' . $chunk . '. Found ' . $sum . ' documents'); + + foreach ($results as $document) { + if ($database->deleteDocument($document->getCollection(), $document->getId())) { + Console::success('Deleted document "' . $document->getId() . '" successfully'); + + if (\is_callable($callback)) { + $callback($document); + } + } else { + Console::error('Failed to delete document: ' . $document->getId()); + } + $count++; + } + } + + $executionEnd = \microtime(true); + + Console::info("Deleted {$count} document by group in " . ($executionEnd - $executionStart) . " seconds"); + } + + protected function trigger( + Document $database, + Document $collection, + Document $attribute, + Document $project, + string $projectId, + array $events + ): void { + $target = Realtime::fromPayload( + // Pass first, most verbose event pattern + event: $events[0], + payload: $attribute, + project: $project, + ); + Realtime::send( + projectId: 'console', + payload: $attribute->getArrayCopy(), + events: $events, + channels: $target['channels'], + roles: $target['roles'], + options: [ + 'projectId' => $projectId, + 'databaseId' => $database->getId(), + 'collectionId' => $collection->getId() + ] + ); + } +} diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php new file mode 100644 index 0000000000..7a20212c9c --- /dev/null +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -0,0 +1,126 @@ +desc('Mails worker') + ->inject('message') + ->inject('register') + ->callback(fn($message, $register) => $this->action($message, $register)); + } + + /** + * @param Message $message + * @param Registry $register + * @throws \PHPMailer\PHPMailer\Exception + * @return void + * @throws Exception + */ + public function action(Message $message, Registry $register): void + { + Runtime::setHookFlags(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP); + $payload = $message->getPayload() ?? []; + + if (empty($payload)) { + throw new Exception('Missing payload'); + } + + $smtp = $payload['smtp']; + + if (empty($smtp) && empty(App::getEnv('_APP_SMTP_HOST'))) { + Console::info('Skipped mail processing. No SMTP configuration has been set.'); + return; + } + + $recipient = $payload['recipient']; + $subject = $payload['subject']; + $variables = $payload['variables']; + $name = $payload['name']; + $body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base.tpl'); + + foreach ($variables as $key => $value) { + $body->setParam('{{' . $key . '}}', $value); + } + + $body = $body->render(); + + /** @var PHPMailer $mail */ + $mail = empty($smtp) + ? $register->get('smtp') + : $this->getMailer($smtp); + + $mail->clearAddresses(); + $mail->clearAllRecipients(); + $mail->clearReplyTos(); + $mail->clearAttachments(); + $mail->clearBCCs(); + $mail->clearCCs(); + $mail->addAddress($recipient, $name); + $mail->Subject = $subject; + $mail->Body = $body; + $mail->AltBody = \strip_tags($body); + + try { + $mail->send(); + } catch (\Exception $error) { + throw new Exception('Error sending mail: ' . $error->getMessage(), 500); + } + } + + /** + * @param array $smtp + * @return PHPMailer + * @throws \PHPMailer\PHPMailer\Exception + */ + protected function getMailer(array $smtp): PHPMailer + { + $mail = new PHPMailer(true); + + $mail->isSMTP(); + + $username = $smtp['username']; + $password = $smtp['password']; + + $mail->XMailer = 'Appwrite Mailer'; + $mail->Host = $smtp['host']; + $mail->Port = $smtp['port']; + $mail->SMTPAuth = (!empty($username) && !empty($password)); + $mail->Username = $username; + $mail->Password = $password; + $mail->SMTPSecure = $smtp['secure']; + $mail->SMTPAutoTLS = false; + $mail->CharSet = 'UTF-8'; + + $mail->setFrom($smtp['senderEmail'], $smtp['senderName']); + + if (!empty($smtp['replyTo'])) { + $mail->addReplyTo($smtp['replyTo'], $smtp['senderName']); + } + + $mail->isHTML(); + + return $mail; + } +} diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php new file mode 100644 index 0000000000..31b0df59a3 --- /dev/null +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -0,0 +1,330 @@ +desc('Migrations worker') + ->inject('message') + ->inject('dbForProject') + ->inject('dbForConsole') + ->callback(fn(Message $message, Database $dbForProject, Database $dbForConsole) => $this->action($message, $dbForProject, $dbForConsole)); + } + + /** + * @param Message $message + * @param Database $dbForProject + * @param Database $dbForConsole + * @return void + * @throws Exception + */ + public function action(Message $message, Database $dbForProject, Database $dbForConsole): void + { + $payload = $message->getPayload() ?? []; + + if (empty($payload)) { + throw new Exception('Missing payload'); + } + + $events = $payload['events'] ?? []; + $project = new Document($payload['project'] ?? []); + $migration = new Document($payload['migration'] ?? []); + + if ($project->getId() === 'console') { + return; + } + + $this->dbForProject = $dbForProject; + $this->dbForConsole = $dbForConsole; + + /** + * Handle Event execution. + */ + if (! empty($events)) { + return; + } + + $this->processMigration($project, $migration); + } + + /** + * @param string $source + * @param array $credentials + * @return Source + * @throws Exception + */ + protected function processSource(string $source, array $credentials): Source + { + return match ($source) { + Firebase::getName() => new Firebase( + json_decode($credentials['serviceAccount'], true), + ), + Supabase::getName() => new Supabase( + $credentials['endpoint'], + $credentials['apiKey'], + $credentials['databaseHost'], + 'postgres', + $credentials['username'], + $credentials['password'], + $credentials['port'], + ), + NHost::getName() => new NHost( + $credentials['subdomain'], + $credentials['region'], + $credentials['adminSecret'], + $credentials['database'], + $credentials['username'], + $credentials['password'], + $credentials['port'], + ), + Appwrite::getName() => new Appwrite($credentials['projectId'], str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], $credentials['apiKey']), + default => throw new \Exception('Invalid source type'), + }; + } + + /** + * @throws Authorization + * @throws Structure + * @throws Conflict + * @throws \Utopia\Database\Exception + * @throws Exception + */ + protected function updateMigrationDocument(Document $migration, Document $project): Document + { + /** Trigger Realtime */ + $allEvents = Event::generateEvents('migrations.[migrationId].update', [ + 'migrationId' => $migration->getId(), + ]); + + $target = Realtime::fromPayload( + event: $allEvents[0], + payload: $migration, + project: $project + ); + + Realtime::send( + projectId: 'console', + payload: $migration->getArrayCopy(), + events: $allEvents, + channels: $target['channels'], + roles: $target['roles'], + ); + + Realtime::send( + projectId: $project->getId(), + payload: $migration->getArrayCopy(), + events: $allEvents, + channels: $target['channels'], + roles: $target['roles'], + ); + + return $this->dbForProject->updateDocument('migrations', $migration->getId(), $migration); + } + + /** + * @param Document $apiKey + * @return void + * @throws \Utopia\Database\Exception + * @throws Authorization + * @throws Conflict + * @throws Restricted + * @throws Structure + */ + protected function removeAPIKey(Document $apiKey): void + { + $this->dbForConsole->deleteDocument('keys', $apiKey->getId()); + } + + /** + * @param Document $project + * @return Document + * @throws Authorization + * @throws Structure + * @throws \Utopia\Database\Exception + * @throws Exception + */ + protected function generateAPIKey(Document $project): Document + { + $generatedSecret = bin2hex(\random_bytes(128)); + + $key = new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'projectInternalId' => $project->getInternalId(), + 'projectId' => $project->getId(), + 'name' => 'Transfer API Key', + 'scopes' => [ + 'users.read', + 'users.write', + 'teams.read', + 'teams.write', + 'databases.read', + 'databases.write', + 'collections.read', + 'collections.write', + 'documents.read', + 'documents.write', + 'buckets.read', + 'buckets.write', + 'files.read', + 'files.write', + 'functions.read', + 'functions.write', + ], + 'expire' => null, + 'sdks' => [], + 'accessedAt' => null, + 'secret' => $generatedSecret, + ]); + + $this->dbForConsole->createDocument('keys', $key); + $this->dbForConsole->deleteCachedDocument('projects', $project->getId()); + + return $key; + } + + /** + * @param Document $project + * @param Document $migration + * @return void + * @throws Authorization + * @throws Conflict + * @throws Restricted + * @throws Structure + * @throws \Utopia\Database\Exception + */ + protected function processMigration(Document $project, Document $migration): void + { + /** + * @var Document $migrationDocument + * @var Transfer $transfer + */ + $migrationDocument = null; + $transfer = null; + $projectDocument = $this->dbForConsole->getDocument('projects', $project->getId()); + $tempAPIKey = $this->generateAPIKey($projectDocument); + + try { + $migrationDocument = $this->dbForProject->getDocument('migrations', $migration->getId()); + $migrationDocument->setAttribute('stage', 'processing'); + $migrationDocument->setAttribute('status', 'processing'); + $this->updateMigrationDocument($migrationDocument, $projectDocument); + + $source = $this->processSource($migrationDocument->getAttribute('source'), $migrationDocument->getAttribute('credentials')); + + $source->report(); + + $destination = new DestinationsAppwrite( + $projectDocument->getId(), + 'http://appwrite/v1', + $tempAPIKey['secret'], + ); + + $transfer = new Transfer( + $source, + $destination + ); + + /** Start Transfer */ + $migrationDocument->setAttribute('stage', 'migrating'); + $this->updateMigrationDocument($migrationDocument, $projectDocument); + $transfer->run($migrationDocument->getAttribute('resources'), function () use ($migrationDocument, $transfer, $projectDocument) { + $migrationDocument->setAttribute('resourceData', json_encode($transfer->getCache())); + $migrationDocument->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); + + $this->updateMigrationDocument($migrationDocument, $projectDocument); + }); + + $errors = $transfer->getReport(Resource::STATUS_ERROR); + + if (count($errors) > 0) { + $migrationDocument->setAttribute('status', 'failed'); + $migrationDocument->setAttribute('stage', 'finished'); + + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[] = "Failed to transfer resource '{$error['id']}:{$error['resource']}' with message '{$error['message']}'"; + } + + $migrationDocument->setAttribute('errors', $errorMessages); + $this->updateMigrationDocument($migrationDocument, $projectDocument); + + return; + } + + $migrationDocument->setAttribute('status', 'completed'); + $migrationDocument->setAttribute('stage', 'finished'); + } catch (\Throwable $th) { + Console::error($th->getMessage()); + + if ($migrationDocument) { + Console::error($th->getMessage()); + Console::error($th->getTraceAsString()); + $migrationDocument->setAttribute('status', 'failed'); + $migrationDocument->setAttribute('stage', 'finished'); + $migrationDocument->setAttribute('errors', [$th->getMessage()]); + + return; + } + + if ($transfer) { + $errors = $transfer->getReport(Resource::STATUS_ERROR); + + if (count($errors) > 0) { + $migrationDocument->setAttribute('status', 'failed'); + $migrationDocument->setAttribute('stage', 'finished'); + $migrationDocument->setAttribute('errors', $errors); + } + } + } finally { + if ($migrationDocument) { + $this->updateMigrationDocument($migrationDocument, $projectDocument); + } + if ($tempAPIKey) { + $this->removeAPIKey($tempAPIKey); + } + } + } +} diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php new file mode 100644 index 0000000000..dd7b92bf5e --- /dev/null +++ b/src/Appwrite/Platform/Workers/Webhooks.php @@ -0,0 +1,118 @@ +desc('Webhooks worker') + ->inject('message') + ->callback(fn($message) => $this->action($message)); + } + + /** + * @param Message $message + * @return void + * @throws Exception + */ + public function action(Message $message): void + { + $payload = $message->getPayload() ?? []; + + if (empty($payload)) { + throw new Exception('Missing payload'); + } + + $events = $payload['events']; + $webhookPayload = json_encode($payload['payload']); + $project = new Document($payload['project']); + $user = new Document($payload['user'] ?? []); + + foreach ($project->getAttribute('webhooks', []) as $webhook) { + if (array_intersect($webhook->getAttribute('events', []), $events)) { + $this->execute($events, $webhookPayload, $webhook, $user, $project); + } + } + + if (!empty($this->errors)) { + throw new Exception(\implode(" / \n\n", $this->errors)); + } + } + + /** + * @param array $events + * @param string $payload + * @param Document $webhook + * @param Document $user + * @param Document $project + * @return void + */ + private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project): void + { + + $url = \rawurldecode($webhook->getAttribute('url')); + $signatureKey = $webhook->getAttribute('signatureKey'); + $signature = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true)); + $httpUser = $webhook->getAttribute('httpUser'); + $httpPass = $webhook->getAttribute('httpPass'); + $ch = \curl_init($webhook->getAttribute('url')); + + \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + \curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + \curl_setopt($ch, CURLOPT_HEADER, 0); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, CURLOPT_USERAGENT, \sprintf( + APP_USERAGENT, + App::getEnv('_APP_VERSION', 'UNKNOWN'), + App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) + )); + \curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + [ + 'Content-Type: application/json', + 'Content-Length: ' . \strlen($payload), + 'X-' . APP_NAME . '-Webhook-Id: ' . $webhook->getId(), + 'X-' . APP_NAME . '-Webhook-Events: ' . implode(',', $events), + 'X-' . APP_NAME . '-Webhook-Name: ' . $webhook->getAttribute('name', ''), + 'X-' . APP_NAME . '-Webhook-User-Id: ' . $user->getId(), + 'X-' . APP_NAME . '-Webhook-Project-Id: ' . $project->getId(), + 'X-' . APP_NAME . '-Webhook-Signature: ' . $signature, + ] + ); + + if (!$webhook->getAttribute('security', true)) { + \curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + \curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + + if (!empty($httpUser) && !empty($httpPass)) { + \curl_setopt($ch, CURLOPT_USERPWD, "$httpUser:$httpPass"); + \curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + } + + if (false === \curl_exec($ch)) { + $this->errors[] = \curl_error($ch) . ' in events ' . implode(', ', $events) . ' for webhook ' . $webhook->getAttribute('name'); + } + + \curl_close($ch); + } +} From 2884cb636b7803fd075150b15e7b940c347bdb73 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 21 Oct 2023 07:18:21 -0400 Subject: [PATCH 04/61] cleanup --- app/console | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/console b/app/console index e965738987..9b4bcb8140 160000 --- a/app/console +++ b/app/console @@ -1 +1 @@ -Subproject commit e9657389879c8d76a9b3a0d3486c1d86f43c3bb9 +Subproject commit 9b4bcb8140484669421685b4ba89fa1c4d331360 From 77a43006690c2dab46b7f059219621a11f419c0b Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 21 Oct 2023 07:22:46 -0400 Subject: [PATCH 05/61] cleanups --- package 2.json | 8 -------- package-lock.json | 10 ---------- pnpm-lock.yaml | 5 ----- 3 files changed, 23 deletions(-) delete mode 100644 package 2.json delete mode 100644 package-lock.json delete mode 100644 pnpm-lock.yaml diff --git a/package 2.json b/package 2.json deleted file mode 100644 index 6e32c7d515..0000000000 --- a/package 2.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "private": true, - "name": "@appwrite.io/repo", - "repository": { - "type": "git", - "url": "git+https://github.com/appwrite/appwrite.git" - } -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6ab33b14fe..0000000000 --- a/package-lock.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@appwrite.io/repo", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@appwrite.io/repo" - } - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 2b9f1883a1..0000000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,5 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false From 92538c57f6570712907cfc8a015a13e5d34a3004 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 21 Oct 2023 07:29:23 -0400 Subject: [PATCH 06/61] cleanups --- app/test | 184 ------------------------------------------------------- 1 file changed, 184 deletions(-) delete mode 100644 app/test diff --git a/app/test b/app/test deleted file mode 100644 index a4d28b1200..0000000000 --- a/app/test +++ /dev/null @@ -1,184 +0,0 @@ -$register->set('pools', function () { - $group = new Group(); - - $fallbackForDB = 'db_main=' . AppwriteURL::unparse([ - 'scheme' => 'mariadb', - 'host' => App::getEnv('_APP_DB_HOST', 'mariadb'), - 'port' => App::getEnv('_APP_DB_PORT', '3306'), - 'user' => App::getEnv('_APP_DB_USER', ''), - 'pass' => App::getEnv('_APP_DB_PASS', ''), - 'path' => App::getEnv('_APP_DB_SCHEMA', ''), - ]); - $fallbackForRedis = 'redis_main=' . AppwriteURL::unparse([ - 'scheme' => 'redis', - 'host' => App::getEnv('_APP_REDIS_HOST', 'redis'), - 'port' => App::getEnv('_APP_REDIS_PORT', '6379'), - 'user' => App::getEnv('_APP_REDIS_USER', ''), - 'pass' => App::getEnv('_APP_REDIS_PASS', ''), - ]); - - $connections = [ - 'console' => [ - 'type' => 'database', - 'dsns' => App::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB), - 'multiple' => false, - 'schemes' => ['mariadb', 'mysql'], - ], - 'database' => [ - 'type' => 'database', - 'dsns' => App::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB), - 'multiple' => true, - 'schemes' => ['mariadb', 'mysql'], - ], - 'queue' => [ - 'type' => 'queue', - 'dsns' => App::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis), - 'multiple' => false, - 'schemes' => ['redis'], - ], - 'pubsub' => [ - 'type' => 'pubsub', - 'dsns' => App::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis), - 'multiple' => false, - 'schemes' => ['redis'], - ], - 'cache' => [ - 'type' => 'cache', - 'dsns' => App::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis), - 'multiple' => true, - 'schemes' => ['redis'], - ], - ]; - - $maxConnections = App::getEnv('_APP_CONNECTIONS_MAX', 151); - $instanceConnections = $maxConnections / App::getEnv('_APP_POOL_CLIENTS', 14); - - $multiprocessing = App::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled'; - - if ($multiprocessing) { - $workerCount = swoole_cpu_num() * intval(App::getEnv('_APP_WORKER_PER_CORE', 6)); - } else { - $workerCount = 1; - } - - if ($workerCount > $instanceConnections) { - throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500); - } - - $poolSize = (int)($instanceConnections / $workerCount); - - foreach ($connections as $key => $connection) { - $type = $connection['type'] ?? ''; - $dsns = $connection['dsns'] ?? ''; - $multipe = $connection['multiple'] ?? false; - $schemes = $connection['schemes'] ?? []; - $config = []; - $dsns = explode(',', $connection['dsns'] ?? ''); - foreach ($dsns as &$dsn) { - $dsn = explode('=', $dsn); - $name = ($multipe) ? $key . '_' . $dsn[0] : $key; - $dsn = $dsn[1] ?? ''; - $config[] = $name; - if (empty($dsn)) { - //throw new Exception(Exception::GENERAL_SERVER_ERROR, "Missing value for DSN connection in {$key}"); - continue; - } - - $dsn = new DSN($dsn); - $dsnHost = $dsn->getHost(); - $dsnPort = $dsn->getPort(); - $dsnUser = $dsn->getUser(); - $dsnPass = $dsn->getPassword(); - $dsnScheme = $dsn->getScheme(); - $dsnDatabase = $dsn->getPath(); - - if (!in_array($dsnScheme, $schemes)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid console database scheme"); - } - - /** - * Get Resource - * - * Creation could be reused accross connection types like database, cache, queue, etc. - * - * Resource assignment to an adapter will happen below. - */ - switch ($dsnScheme) { - case 'mysql': - case 'mariadb': - $resource = function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { - return new PDOProxy(function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { - return new PDO("mysql:host={$dsnHost};port={$dsnPort};dbname={$dsnDatabase};charset=utf8mb4", $dsnUser, $dsnPass, array( - PDO::ATTR_TIMEOUT => 3, // Seconds - PDO::ATTR_PERSISTENT => true, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed - PDO::ATTR_EMULATE_PREPARES => true, - PDO::ATTR_STRINGIFY_FETCHES => true - )); - }); - }; - break; - case 'redis': - $resource = function () use ($dsnHost, $dsnPort, $dsnPass) { - $redis = new Redis(); - @$redis->pconnect($dsnHost, (int)$dsnPort); - if ($dsnPass) { - $redis->auth($dsnPass); - } - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - - return $redis; - }; - break; - - default: - throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid scheme"); - break; - } - - $pool = new Pool($name, $poolSize, function () use ($type, $resource, $dsn) { - // Get Adapter - $adapter = null; - switch ($type) { - case 'database': - $adapter = match ($dsn->getScheme()) { - 'mariadb' => new MariaDB($resource()), - 'mysql' => new MySQL($resource()), - default => null - }; - - $adapter->setDefaultDatabase($dsn->getPath()); - break; - case 'pubsub': - $adapter = $resource(); - break; - case 'queue': - $adapter = match ($dsn->getScheme()) { - 'redis' => new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()), - default => null - }; - break; - case 'cache': - $adapter = match ($dsn->getScheme()) { - 'redis' => new RedisCache($resource()), - default => null - }; - break; - - default: - throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation."); - break; - } - - return $adapter; - }); - - $group->add($pool); - } - - Config::setParam('pools-' . $key, $config); - } - - return $group; -}); From f4fedc832e96e978a0d98f1353e9436cb6754d7d Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 21 Oct 2023 08:30:35 -0400 Subject: [PATCH 07/61] Fix formating --- app/http.php | 12 ++++++------ app/init.php | 48 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/app/http.php b/app/http.php index 72b89130b9..0eadb5c1e1 100644 --- a/app/http.php +++ b/app/http.php @@ -333,19 +333,19 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $connectionForQueue = $app->getResource('connectionForQueue'); $connectionsForCache = $app->getResource('connectionsForCache'); - if(!is_null($connectionForConsole)) { + if (!is_null($connectionForConsole)) { $connectionForConsole->reclaim(); } - - if(!is_null($connectionForProject)) { + + if (!is_null($connectionForProject)) { $connectionForProject->reclaim(); } - - if(!is_null($connectionForQueue)) { + + if (!is_null($connectionForQueue)) { $connectionForQueue->reclaim(); } - if(!empty($connectionsForCache)) { + if (!empty($connectionsForCache)) { foreach ($connectionsForCache as $connection) { $connection->reclaim(); } diff --git a/app/init.php b/app/init.php index 77fd558a56..f73879a02b 100644 --- a/app/init.php +++ b/app/init.php @@ -885,8 +885,10 @@ App::setResource('localeCodes', function () { // Queues App::setResource('queue', function (Group $pools) { $connection = $pools->get('queue')->pop(); - - App::setResource('connectionForQueue', function () use ($connection) { return $connection; }, []); + + App::setResource('connectionForQueue', function () use ($connection) { + return $connection; + }, []); return $connection->getResource(); }, ['pools']); @@ -1112,10 +1114,18 @@ App::setResource('console', function () { ]); }, []); -App::setResource('connectionForProject', function () { return null; }, []); -App::setResource('connectionForConsole', function () { return null; }, []); -App::setResource('connectionForQueue', function () { return null; }, []); -App::setResource('connectionsForCache', function () { return []; }, []); +App::setResource('connectionForProject', function () { + return null; +}, []); +App::setResource('connectionForConsole', function () { + return null; +}, []); +App::setResource('connectionForQueue', function () { + return null; +}, []); +App::setResource('connectionsForCache', function () { + return []; +}, []); App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) { if ($project->isEmpty() || $project->getId() === 'console') { @@ -1127,10 +1137,12 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, ->pop() ; - App::setResource('connectionForProject', function () use ($connection) { return $connection; }, []); - + App::setResource('connectionForProject', function () use ($connection) { + return $connection; + }, []); + $dbAdapter = $connection->getResource(); - + $database = new Database($dbAdapter, $cache); $database @@ -1144,9 +1156,11 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) { $connection = $pools ->get('console') ->pop(); - - App::setResource('connectionForConsole', function () use ($connection) { return $connection; }, []); - + + App::setResource('connectionForConsole', function () use ($connection) { + return $connection; + }, []); + $dbAdapter = $connection->getResource(); $database = new Database($dbAdapter, $cache); @@ -1163,7 +1177,7 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, if ($project->isEmpty() || $project->getId() === 'console') { return $dbForConsole; } - + $connection = $pools ->get($project->getAttribute('database')) ->pop() @@ -1171,7 +1185,9 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $dbAdapter = $connection->getResource(); - App::setResource('connectionForProject', function () use ($connection) { return $connection; }, []); + App::setResource('connectionForProject', function () use ($connection) { + return $connection; + }, []); $database = new Database($dbAdapter, $cache); @@ -1200,7 +1216,9 @@ App::setResource('cache', function (Group $pools) { $adapters[] = $connection->getResource(); } - App::setResource('connectionsForCache', function () use ($connections) { return $connections; }, []); + App::setResource('connectionsForCache', function () use ($connections) { + return $connections; + }, []); return new Cache(new Sharding($adapters)); }, ['pools']); From d4afe2239662927b06844bdffdcb2118facb48b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 6 Feb 2024 12:07:47 +0100 Subject: [PATCH 08/61] Add ZDT migration support to project creation --- app/controllers/api/projects.php | 10 +++++++++- src/Appwrite/Hooks/Hooks.php | 5 +++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 8314a21f30..135ce9a50f 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -4,6 +4,7 @@ use Appwrite\Auth\Auth; use Appwrite\Event\Delete; use Appwrite\Event\Validator\Event; use Appwrite\Extend\Exception; +use Appwrite\Hooks\Hooks; use Appwrite\Network\Validator\Email; use Appwrite\Network\Validator\Origin; use Appwrite\Template\Template; @@ -75,7 +76,8 @@ App::post('/v1/projects') ->inject('dbForConsole') ->inject('cache') ->inject('pools') - ->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole, Cache $cache, Group $pools) { + ->inject('hooks') + ->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole, Cache $cache, Group $pools, Hooks $hooks) { $team = $dbForConsole->getDocument('teams', $teamId); @@ -183,6 +185,12 @@ App::post('/v1/projects') /** @var array $collections */ $collections = Config::getParam('collections', [])['projects'] ?? []; + // Allow Cloud overrides (useful for ZDT migration) + $collectiosOverride = $hooks->trigger('getProjectCollections'); + if(!empty($collectiosOverride)) { + $collections = $collectiosOverride; + } + foreach ($collections as $key => $collection) { if (($collection['$collection'] ?? '') !== Database::METADATA) { continue; diff --git a/src/Appwrite/Hooks/Hooks.php b/src/Appwrite/Hooks/Hooks.php index 00d2f5a9e9..1fe17c7a0e 100644 --- a/src/Appwrite/Hooks/Hooks.php +++ b/src/Appwrite/Hooks/Hooks.php @@ -16,11 +16,12 @@ class Hooks /** * @param mixed[] $params + * @return mixed */ - public function trigger(string $name, array $params = []) + public function trigger(string $name, array $params = []): mixed { if (isset(self::$hooks[$name])) { - call_user_func_array(self::$hooks[$name], $params); + return call_user_func_array(self::$hooks[$name], $params); } } } From 8154b4c131eecf417549bb54916c343075108d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 6 Feb 2024 12:11:27 +0100 Subject: [PATCH 09/61] Linter fix --- app/controllers/api/projects.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 135ce9a50f..0f7fad564d 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -187,7 +187,7 @@ App::post('/v1/projects') // Allow Cloud overrides (useful for ZDT migration) $collectiosOverride = $hooks->trigger('getProjectCollections'); - if(!empty($collectiosOverride)) { + if (!empty($collectiosOverride)) { $collections = $collectiosOverride; } From 3a5c233b596676af518f7a668df58710592abcd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 6 Feb 2024 11:47:43 +0000 Subject: [PATCH 10/61] Fix bug --- src/Appwrite/Hooks/Hooks.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Appwrite/Hooks/Hooks.php b/src/Appwrite/Hooks/Hooks.php index 1fe17c7a0e..4840229086 100644 --- a/src/Appwrite/Hooks/Hooks.php +++ b/src/Appwrite/Hooks/Hooks.php @@ -23,5 +23,7 @@ class Hooks if (isset(self::$hooks[$name])) { return call_user_func_array(self::$hooks[$name], $params); } + + return null; } } From a7e95588c6631c298782f51e920e1f8f75191b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 6 Feb 2024 16:08:42 +0100 Subject: [PATCH 11/61] Fix bug on ZDT --- app/controllers/api/projects.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 0f7fad564d..d218750a50 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -185,12 +185,6 @@ App::post('/v1/projects') /** @var array $collections */ $collections = Config::getParam('collections', [])['projects'] ?? []; - // Allow Cloud overrides (useful for ZDT migration) - $collectiosOverride = $hooks->trigger('getProjectCollections'); - if (!empty($collectiosOverride)) { - $collections = $collectiosOverride; - } - foreach ($collections as $key => $collection) { if (($collection['$collection'] ?? '') !== Database::METADATA) { continue; @@ -225,6 +219,8 @@ App::post('/v1/projects') $dbForProject->createCollection($key, $attributes, $indexes); } + $hooks->trigger('afterProjectCreated'); // Useful for ZDT to mirror the project to destination + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($project, Response::MODEL_PROJECT); From 939aec866e17bdd6f5a33e035acc520b3a9f3939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 7 Feb 2024 10:16:17 +0100 Subject: [PATCH 12/61] Add getProxyProjectDatabase hook --- app/controllers/api/projects.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index d218750a50..5318eb4384 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -172,9 +172,14 @@ App::post('/v1/projects') throw new Exception(Exception::PROJECT_ALREADY_EXISTS); } - $dbForProject = new Database($pools->get($database)->pop()->getResource(), $cache); - $dbForProject->setNamespace("_{$project->getInternalId()}"); - $dbForProject->create(); + // Useful for ZDT to mirror the project to destination + $dbForProject = $hooks->trigger('getProxyProjectDatabase', [ $project, $pools, $cache ]); + + if(empty($dbForProject)) { + $dbForProject = new Database($pools->get($database)->pop()->getResource(), $cache); + $dbForProject->setNamespace("_{$project->getInternalId()}"); + $dbForProject->create(); + } $audit = new Audit($dbForProject); $audit->setup(); @@ -219,8 +224,6 @@ App::post('/v1/projects') $dbForProject->createCollection($key, $attributes, $indexes); } - $hooks->trigger('afterProjectCreated'); // Useful for ZDT to mirror the project to destination - $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($project, Response::MODEL_PROJECT); From d5216d5b14588b2b99d51d02ae8b6935864371a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 7 Feb 2024 10:17:34 +0100 Subject: [PATCH 13/61] Linter fix --- app/controllers/api/projects.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 5318eb4384..4ce673c977 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -175,7 +175,7 @@ App::post('/v1/projects') // Useful for ZDT to mirror the project to destination $dbForProject = $hooks->trigger('getProxyProjectDatabase', [ $project, $pools, $cache ]); - if(empty($dbForProject)) { + if (empty($dbForProject)) { $dbForProject = new Database($pools->get($database)->pop()->getResource(), $cache); $dbForProject->setNamespace("_{$project->getInternalId()}"); $dbForProject->create(); From a89b73d20bcf489d97b10e3b573d7fa543a770dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 7 Feb 2024 10:48:02 +0100 Subject: [PATCH 14/61] Fix bug --- app/controllers/api/projects.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 4ce673c977..4613070611 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -178,9 +178,10 @@ App::post('/v1/projects') if (empty($dbForProject)) { $dbForProject = new Database($pools->get($database)->pop()->getResource(), $cache); $dbForProject->setNamespace("_{$project->getInternalId()}"); - $dbForProject->create(); } + $dbForProject->create(); + $audit = new Audit($dbForProject); $audit->setup(); From abb10073853071e28abc006a0f8e2ff0d920af1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 7 Feb 2024 12:04:46 +0100 Subject: [PATCH 15/61] Rework project creation --- app/controllers/api/projects.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 4613070611..4fb95cf175 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -172,14 +172,8 @@ App::post('/v1/projects') throw new Exception(Exception::PROJECT_ALREADY_EXISTS); } - // Useful for ZDT to mirror the project to destination - $dbForProject = $hooks->trigger('getProxyProjectDatabase', [ $project, $pools, $cache ]); - - if (empty($dbForProject)) { - $dbForProject = new Database($pools->get($database)->pop()->getResource(), $cache); - $dbForProject->setNamespace("_{$project->getInternalId()}"); - } - + $dbForProject = new Database($pools->get($database)->pop()->getResource(), $cache); + $dbForProject->setNamespace("_{$project->getInternalId()}"); $dbForProject->create(); $audit = new Audit($dbForProject); @@ -225,6 +219,8 @@ App::post('/v1/projects') $dbForProject->createCollection($key, $attributes, $indexes); } + $hooks->trigger('afterProjectCreation', [ $project, $pools, $cache ]); + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($project, Response::MODEL_PROJECT); From 219b28e9bfdcfd4da9b0e22e3ac2e1646879dc27 Mon Sep 17 00:00:00 2001 From: shimon Date: Tue, 13 Feb 2024 16:08:40 +0200 Subject: [PATCH 16/61] usage updates --- bin/worker-usage-dump | 3 + docker-compose.yml | 32 ++++++ src/Appwrite/Platform/Workers/Usage.php | 57 +++++++--- src/Appwrite/Platform/Workers/UsageDump.php | 113 ++++++++++++++++++++ src/Appwrite/Platform/Workers/UsageHook.php | 103 ------------------ 5 files changed, 188 insertions(+), 120 deletions(-) create mode 100644 bin/worker-usage-dump create mode 100644 src/Appwrite/Platform/Workers/UsageDump.php delete mode 100644 src/Appwrite/Platform/Workers/UsageHook.php diff --git a/bin/worker-usage-dump b/bin/worker-usage-dump new file mode 100644 index 0000000000..43ca87fcb3 --- /dev/null +++ b/bin/worker-usage-dump @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/worker.php usage-dump $@ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 395923681d..c4334bb45c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -677,6 +677,38 @@ services: - _APP_LOGGING_CONFIG - _APP_USAGE_AGGREGATION_INTERVAL + appwrite-worker-usage-dump: + entrypoint: worker-usage-dump + <<: *x-logging + container_name: appwrite-worker-usage-dump + image: appwrite-dev + networks: + - appwrite + volumes: + - ./app:/usr/src/code/app + - ./src:/usr/src/code/src + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_USAGE_STATS + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG + - _APP_USAGE_AGGREGATION_INTERVAL + + appwrite-schedule: entrypoint: schedule <<: *x-logging diff --git a/src/Appwrite/Platform/Workers/Usage.php b/src/Appwrite/Platform/Workers/Usage.php index 3809d000f7..49f3344906 100644 --- a/src/Appwrite/Platform/Workers/Usage.php +++ b/src/Appwrite/Platform/Workers/Usage.php @@ -3,23 +3,23 @@ namespace Appwrite\Platform\Workers; use Exception; +use Utopia\App; use Utopia\CLI\Console; -use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Platform\Action; +use Appwrite\Event\UsageDump; use Utopia\Queue\Message; class Usage extends Action { - protected static array $stats = []; - protected array $periods = [ - '1h' => 'Y-m-d H:00', - '1d' => 'Y-m-d 00:00', - 'inf' => '0000-00-00 00:00' - ]; + private array $stats = []; + private int $lastTriggeredTime = 0; + private int $keys = 0; + private const INFINITY_PERIOD = '_inf_'; + private const KEYS_THRESHOLD = 10000; + - protected const INFINITY_PERIOD = '_inf_'; public static function getName(): string { return 'usage'; @@ -35,26 +35,31 @@ class Usage extends Action ->desc('Usage worker') ->inject('message') ->inject('getProjectDB') - ->callback(function (Message $message, callable $getProjectDB) { - $this->action($message, $getProjectDB); + ->inject('queueForUsageDump') + ->callback(function (Message $message, callable $getProjectDB, UsageDump $queueForUsageDump) { + $this->action($message, $getProjectDB, $queueForUsageDump); }); + + $this->lastTriggeredTime = time(); } /** * @param Message $message * @param callable $getProjectDB + * @param UsageDump $queueForUsageDump * @return void * @throws \Utopia\Database\Exception * @throws Exception */ - public function action(Message $message, callable $getProjectDB): void + public function action(Message $message, callable $getProjectDB, UsageDump $queueForUsageDump): void { $payload = $message->getPayload() ?? []; if (empty($payload)) { throw new Exception('Missing payload'); } + //Todo Figure out way to preserve keys when the container is being recreated @shimonewman - $payload = $message->getPayload() ?? []; + $aggregationInterval = (int) App::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '20'); $project = new Document($payload['project'] ?? []); $projectId = $project->getInternalId(); foreach ($payload['reduce'] ?? [] as $document) { @@ -69,17 +74,35 @@ class Usage extends Action getProjectDB: $getProjectDB ); } - self::$stats[$projectId]['project'] = $project; + + $this->stats[$projectId]['project'] = $project; foreach ($payload['metrics'] ?? [] as $metric) { - if (!isset(self::$stats[$projectId]['keys'][$metric['key']])) { - self::$stats[$projectId]['keys'][$metric['key']] = $metric['value']; + $this->keys++; + if (!isset($this->stats[$projectId]['keys'][$metric['key']])) { + $this->stats[$projectId]['keys'][$metric['key']] = $metric['value']; continue; } - self::$stats[$projectId]['keys'][$metric['key']] += $metric['value']; + + $this->stats[$projectId]['keys'][$metric['key']] += $metric['value']; + } + + // if keys crossed threshold or X time passed since the last send and there are some keys in the array ($this->stats) + if ( + $this->keys >= self::KEYS_THRESHOLD || + (time() - $this->lastTriggeredTime > $aggregationInterval && $this->keys > 0) + ) { + console::warning('[' . DateTime::now() . '] Aggregated ' . $this->keys . ' keys'); + + $queueForUsageDump + ->setStats($this->stats) + ->trigger(); + + $this->stats = []; + $this->keys = 0; + $this->lastTriggeredTime = time(); } } - /** * On Documents that tied by relations like functions>deployments>build || documents>collection>database || buckets>files. * When we remove a parent document we need to deduct his children aggregation from the project scope. diff --git a/src/Appwrite/Platform/Workers/UsageDump.php b/src/Appwrite/Platform/Workers/UsageDump.php new file mode 100644 index 0000000000..f563578984 --- /dev/null +++ b/src/Appwrite/Platform/Workers/UsageDump.php @@ -0,0 +1,113 @@ + 'Y-m-d H:00', + '1d' => 'Y-m-d 00:00', + 'inf' => '0000-00-00 00:00' + ]; + + public static function getName(): string + { + return 'usage-dump'; + } + + /** + * @throws \Exception + */ + public function __construct() + { + + $this + ->inject('message') + ->inject('getProjectDB') + ->callback(function (Message $message, callable $getProjectDB) { + $this->action($message, $getProjectDB); + }) + ; + } + + /** + * @param Message $message + * @param callable $getProjectDB + * @return void + * @throws Exception + * @throws \Utopia\Database\Exception + */ + public function action(Message $message, callable $getProjectDB): void + { + + $payload = $message->getPayload() ?? []; + if (empty($payload)) { + throw new Exception('Missing payload'); + } + + //Todo rename both usage workers @shimonewman + foreach ($payload['stats'] ?? [] as $stats) { + $project = new Document($stats['project'] ?? []); + $numberOfKeys = !empty($stats['keys']) ? count($stats['keys']) : 0; + + if ($numberOfKeys === 0) { + continue; + } + + console::log('[' . DateTime::now() . '] ProjectId [' . $project->getInternalId() . '] Database [' . $project['database'] . '] ' . $numberOfKeys . ' keys'); + + try { + $dbForProject = $getProjectDB($project); + foreach ($stats['keys'] ?? [] as $key => $value) { + if ($value == 0) { + continue; + } + + foreach ($this->periods as $period => $format) { + $time = 'inf' === $period ? null : date($format, time()); + $id = \md5("{$time}_{$period}_{$key}"); + + try { + $dbForProject->createDocument('stats_v2', new Document([ + '$id' => $id, + 'period' => $period, + 'time' => $time, + 'metric' => $key, + 'value' => $value, + 'region' => App::getEnv('_APP_REGION', 'default'), + ])); + } catch (Duplicate $th) { + if ($value < 0) { + $dbForProject->decreaseDocumentAttribute( + 'stats_v2', + $id, + 'value', + abs($value) + ); + } else { + $dbForProject->increaseDocumentAttribute( + 'stats_v2', + $id, + 'value', + $value + ); + } + } + } + } + } catch (\Exception $e) { + console::error('[' . DateTime::now() . '] project [' . $project->getInternalId() . '] database [' . $project['database'] . '] ' . ' ' . $e->getMessage()); + } + } + } +} diff --git a/src/Appwrite/Platform/Workers/UsageHook.php b/src/Appwrite/Platform/Workers/UsageHook.php deleted file mode 100644 index 4781b1e892..0000000000 --- a/src/Appwrite/Platform/Workers/UsageHook.php +++ /dev/null @@ -1,103 +0,0 @@ -setType(Action::TYPE_WORKER_START) - ->inject('register') - ->inject('getProjectDB') - ->callback(function ($register, callable $getProjectDB) { - $this->action($register, $getProjectDB); - }) - ; - } - - /** - * @param $register - * @param $getProjectDB - * @return void - */ - public function action($register, $getProjectDB): void - { - - $interval = (int) App::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '60000'); - Timer::tick($interval, function () use ($register, $getProjectDB) { - - $offset = count(self::$stats); - $projects = array_slice(self::$stats, 0, $offset, true); - array_splice(self::$stats, 0, $offset); - foreach ($projects as $data) { - $numberOfKeys = !empty($data['keys']) ? count($data['keys']) : 0; - $projectInternalId = $data['project']->getInternalId(); - $database = $data['project']['database'] ?? ''; - - console::warning('Ticker started ' . DateTime::now()); - - if ($numberOfKeys === 0) { - continue; - } - - try { - $dbForProject = $getProjectDB($data['project']); - foreach ($data['keys'] ?? [] as $key => $value) { - if ($value == 0) { - continue; - } - - foreach ($this->periods as $period => $format) { - $time = 'inf' === $period ? null : date($format, time()); - $id = \md5("{$time}_{$period}_{$key}"); - - try { - $dbForProject->createDocument('stats_v2', new Document([ - '$id' => $id, - 'period' => $period, - 'time' => $time, - 'metric' => $key, - 'value' => $value, - 'region' => App::getEnv('_APP_REGION', 'default'), - ])); - } catch (Duplicate $th) { - if ($value < 0) { - $dbForProject->decreaseDocumentAttribute( - 'stats_v2', - $id, - 'value', - abs($value) - ); - } else { - $dbForProject->increaseDocumentAttribute( - 'stats_v2', - $id, - 'value', - $value - ); - } - } - } - } - } catch (\Exception $e) { - console::error(DateTime::now() . ' ' . $projectInternalId . ' ' . $e->getMessage()); - } - } - }); - } -} From e1e5f5d96b0c3d9dba1c4f93f7914636d83f72bd Mon Sep 17 00:00:00 2001 From: shimon Date: Tue, 13 Feb 2024 16:27:01 +0200 Subject: [PATCH 17/61] usage updates --- src/Appwrite/Platform/Services/Workers.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Services/Workers.php b/src/Appwrite/Platform/Services/Workers.php index 6573b3124c..22e6dcd56a 100644 --- a/src/Appwrite/Platform/Services/Workers.php +++ b/src/Appwrite/Platform/Services/Workers.php @@ -14,7 +14,7 @@ use Appwrite\Platform\Workers\Builds; use Appwrite\Platform\Workers\Deletes; use Appwrite\Platform\Workers\Hamster; use Appwrite\Platform\Workers\Usage; -use Appwrite\Platform\Workers\UsageHook; +use Appwrite\Platform\Workers\UsageDump; use Appwrite\Platform\Workers\Migrations; class Workers extends Service @@ -33,7 +33,7 @@ class Workers extends Service ->addAction(Builds::getName(), new Builds()) ->addAction(Deletes::getName(), new Deletes()) ->addAction(Hamster::getName(), new Hamster()) - ->addAction(UsageHook::getName(), new UsageHook()) + ->addAction(UsageDump::getName(), new UsageDump()) ->addAction(Usage::getName(), new Usage()) ->addAction(Migrations::getName(), new Migrations()) From 7d38860d16052ce2a144cc332c26d8f92936691c Mon Sep 17 00:00:00 2001 From: shimon Date: Tue, 13 Feb 2024 17:04:16 +0200 Subject: [PATCH 18/61] usage updates --- app/worker.php | 15 +++++----- src/Appwrite/Event/Event.php | 3 ++ src/Appwrite/Event/UsageDump.php | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 src/Appwrite/Event/UsageDump.php diff --git a/app/worker.php b/app/worker.php index 1b1f4b9f93..4cf0edbae6 100644 --- a/app/worker.php +++ b/app/worker.php @@ -14,6 +14,7 @@ use Appwrite\Event\Mail; use Appwrite\Event\Migration; use Appwrite\Event\Phone; use Appwrite\Event\Usage; +use Appwrite\Event\UsageDump; use Appwrite\Platform\Appwrite; use Swoole\Runtime; use Utopia\App; @@ -146,6 +147,9 @@ Server::setResource('log', fn() => new Log()); Server::setResource('queueForUsage', function (Connection $queue) { return new Usage($queue); }, ['queue']); +Server::setResource('queueForUsageDump', function (Connection $queue) { + return new UsageDump($queue); +}, ['queue']); Server::setResource('queue', function (Group $pools) { return $pools->get('queue')->pop()->getResource(); }, ['pools']); @@ -299,12 +303,9 @@ $worker Console::error('[Error] Line: ' . $error->getLine()); }); -try { - $workerStart = $worker->getWorkerStart(); -} catch (\Throwable $error) { - $worker->workerStart(); -} finally { - Console::info("Worker $workerName started"); -} +$worker->workerStart() + ->action(function () use ($workerName) { + Console::info("Worker $workerName started"); + }); $worker->start(); diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index fc12c5b5b3..9f71ef5ebc 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -27,6 +27,9 @@ class Event public const USAGE_QUEUE_NAME = 'v1-usage'; public const USAGE_CLASS_NAME = 'UsageV1'; + public const USAGE_DUMP_QUEUE_NAME = 'v1-usage-dump'; + public const USAGE_DUMP_CLASS_NAME = 'UsageDumpV1'; + public const WEBHOOK_QUEUE_NAME = 'v1-webhooks'; public const WEBHOOK_CLASS_NAME = 'WebhooksV1'; diff --git a/src/Appwrite/Event/UsageDump.php b/src/Appwrite/Event/UsageDump.php new file mode 100644 index 0000000000..8f87908849 --- /dev/null +++ b/src/Appwrite/Event/UsageDump.php @@ -0,0 +1,47 @@ +setQueue(Event::USAGE_DUMP_QUEUE_NAME) + ->setClass(Event::USAGE_DUMP_CLASS_NAME); + } + + /** + * Add Stats. + * + * @param array $stats + * @return self + */ + public function setStats(array $stats): self + { + $this->stats = $stats; + + return $this; + } + + /** + * Sends metrics to the usage worker. + * + * @return string|bool + */ + public function trigger(): string|bool + { + $client = new Client($this->queue, $this->connection); + + return $client->enqueue([ + 'stats' => $this->stats, + ]); + } +} From 84e4748625ea43077983f2a1c9ee2c6089a1f7bb Mon Sep 17 00:00:00 2001 From: shimon Date: Tue, 13 Feb 2024 17:58:27 +0200 Subject: [PATCH 19/61] usage updates --- .env | 2 +- Dockerfile | 3 ++- src/Appwrite/Platform/Workers/Usage.php | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env b/.env index 31474bf0b6..8a48328f9e 100644 --- a/.env +++ b/.env @@ -77,7 +77,7 @@ _APP_MAINTENANCE_RETENTION_CACHE=2592000 _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_AUDIT=1209600 -_APP_USAGE_AGGREGATION_INTERVAL=60000 +_APP_USAGE_AGGREGATION_INTERVAL=30 _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000 _APP_MAINTENANCE_RETENTION_SCHEDULES=86400 _APP_USAGE_STATS=enabled diff --git a/Dockerfile b/Dockerfile index 9f3ee9b700..5d06d6c828 100755 --- a/Dockerfile +++ b/Dockerfile @@ -99,7 +99,8 @@ RUN chmod +x /usr/local/bin/doctor && \ chmod +x /usr/local/bin/worker-webhooks && \ chmod +x /usr/local/bin/worker-migrations && \ chmod +x /usr/local/bin/worker-hamster && \ - chmod +x /usr/local/bin/worker-usage + chmod +x /usr/local/bin/worker-usage && \ + chmod +x /usr/local/bin/worker-usage-dump # Cloud Executabless diff --git a/src/Appwrite/Platform/Workers/Usage.php b/src/Appwrite/Platform/Workers/Usage.php index 49f3344906..742c404a17 100644 --- a/src/Appwrite/Platform/Workers/Usage.php +++ b/src/Appwrite/Platform/Workers/Usage.php @@ -46,7 +46,6 @@ class Usage extends Action /** * @param Message $message * @param callable $getProjectDB - * @param UsageDump $queueForUsageDump * @return void * @throws \Utopia\Database\Exception * @throws Exception From 207811a224e4891c774c890919a1d31d3a3d4e10 Mon Sep 17 00:00:00 2001 From: shimon Date: Tue, 13 Feb 2024 19:26:23 +0200 Subject: [PATCH 20/61] usage updates --- composer.lock | 83 +++++++++++-------------- src/Appwrite/Platform/Workers/Usage.php | 1 + 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/composer.lock b/composer.lock index 7507243862..ce4c6afca8 100644 --- a/composer.lock +++ b/composer.lock @@ -815,16 +815,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", "shasum": "" }, "require": { @@ -832,9 +832,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -878,7 +875,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -894,7 +891,7 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "utopia-php/abuse", @@ -1190,16 +1187,16 @@ }, { "name": "utopia-php/database", - "version": "0.45.5", + "version": "0.45.6", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "0b66a017f817a910acb83e6aea92bccea9571fe6" + "reference": "c7cc6d57683a4c13d9772dbeea343adb72c443fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/0b66a017f817a910acb83e6aea92bccea9571fe6", - "reference": "0b66a017f817a910acb83e6aea92bccea9571fe6", + "url": "https://api.github.com/repos/utopia-php/database/zipball/c7cc6d57683a4c13d9772dbeea343adb72c443fd", + "reference": "c7cc6d57683a4c13d9772dbeea343adb72c443fd", "shasum": "" }, "require": { @@ -1240,9 +1237,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.45.5" + "source": "https://github.com/utopia-php/database/tree/0.45.6" }, - "time": "2024-01-08T17:08:15+00:00" + "time": "2024-02-01T02:33:43+00:00" }, { "name": "utopia-php/domains", @@ -1353,16 +1350,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.1", + "version": "0.33.2", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "b745607aa1875554a0ad52e28f6db918da1ce11c" + "reference": "b1423ca3e3b61c6c4c2e619d2cb80672809a19f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/b745607aa1875554a0ad52e28f6db918da1ce11c", - "reference": "b745607aa1875554a0ad52e28f6db918da1ce11c", + "url": "https://api.github.com/repos/utopia-php/http/zipball/b1423ca3e3b61c6c4c2e619d2cb80672809a19f3", + "reference": "b1423ca3e3b61c6c4c2e619d2cb80672809a19f3", "shasum": "" }, "require": { @@ -1392,9 +1389,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.1" + "source": "https://github.com/utopia-php/http/tree/0.33.2" }, - "time": "2024-01-17T16:48:32+00:00" + "time": "2024-01-31T10:35:59+00:00" }, { "name": "utopia-php/image", @@ -2471,16 +2468,16 @@ }, { "name": "doctrine/deprecations", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931" + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931", - "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", "shasum": "" }, "require": { @@ -2512,9 +2509,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.2" + "source": "https://github.com/doctrine/deprecations/tree/1.1.3" }, - "time": "2023-09-27T20:04:15+00:00" + "time": "2024-01-30T19:34:25+00:00" }, { "name": "doctrine/instantiator", @@ -4772,16 +4769,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", "shasum": "" }, "require": { @@ -4795,9 +4792,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -4834,7 +4828,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" }, "funding": [ { @@ -4850,20 +4844,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "shasum": "" }, "require": { @@ -4877,9 +4871,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -4917,7 +4908,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" }, "funding": [ { @@ -4933,7 +4924,7 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "textalk/websocket", @@ -5133,5 +5124,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } diff --git a/src/Appwrite/Platform/Workers/Usage.php b/src/Appwrite/Platform/Workers/Usage.php index 742c404a17..49f3344906 100644 --- a/src/Appwrite/Platform/Workers/Usage.php +++ b/src/Appwrite/Platform/Workers/Usage.php @@ -46,6 +46,7 @@ class Usage extends Action /** * @param Message $message * @param callable $getProjectDB + * @param UsageDump $queueForUsageDump * @return void * @throws \Utopia\Database\Exception * @throws Exception From 2ae9d0b710211dd7422cac1da693c42016f7d7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 20 Mar 2024 13:47:20 +0100 Subject: [PATCH 21/61] Make realtime resources configurable --- app/realtime.php | 82 ++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index f7fc7070a4..652b6d9a28 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -36,54 +36,60 @@ require_once __DIR__ . '/init.php'; Runtime::enableCoroutine(SWOOLE_HOOK_ALL); -function getConsoleDB(): Database -{ - global $register; +// Allows overriding +if(!function_exists("getConsoleDB")) { + function getConsoleDB(): Database + { + global $register; - /** @var \Utopia\Pools\Group $pools */ - $pools = $register->get('pools'); + /** @var \Utopia\Pools\Group $pools */ + $pools = $register->get('pools'); - $dbAdapter = $pools - ->get('console') - ->pop() - ->getResource() - ; + $dbAdapter = $pools + ->get('console') + ->pop() + ->getResource() + ; - $database = new Database($dbAdapter, getCache()); + $database = new Database($dbAdapter, getCache()); - $database - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', '_console'); + $database + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', '_console'); - return $database; + return $database; + } } -function getProjectDB(Document $project): Database -{ - global $register; +// Allows overriding +if(!function_exists("getProjectDB")) { + function getProjectDB(Document $project): Database + { + global $register; - /** @var \Utopia\Pools\Group $pools */ - $pools = $register->get('pools'); + /** @var \Utopia\Pools\Group $pools */ + $pools = $register->get('pools'); - if ($project->isEmpty() || $project->getId() === 'console') { - return getConsoleDB(); + if ($project->isEmpty() || $project->getId() === 'console') { + return getConsoleDB(); + } + + $dbAdapter = $pools + ->get($project->getAttribute('database')) + ->pop() + ->getResource() + ; + + $database = new Database($dbAdapter, getCache()); + + $database + ->setNamespace('_' . $project->getInternalId()) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()); + + return $database; } - - $dbAdapter = $pools - ->get($project->getAttribute('database')) - ->pop() - ->getResource() - ; - - $database = new Database($dbAdapter, getCache()); - - $database - ->setNamespace('_' . $project->getInternalId()) - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()); - - return $database; } function getCache(): Cache From 440d924518b1d237dc3db6024372af77b732d1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 20 Mar 2024 14:14:23 +0100 Subject: [PATCH 22/61] Allow getCache override --- app/realtime.php | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 652b6d9a28..c97eb598b8 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -92,24 +92,27 @@ if(!function_exists("getProjectDB")) { } } -function getCache(): Cache -{ - global $register; +// Allows overriding +if(!function_exists("getCache")) { + function getCache(): Cache + { + global $register; - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ - $list = Config::getParam('pools-cache', []); - $adapters = []; + $list = Config::getParam('pools-cache', []); + $adapters = []; - foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; + foreach ($list as $value) { + $adapters[] = $pools + ->get($value) + ->pop() + ->getResource() + ; + } + + return new Cache(new Sharding($adapters)); } - - return new Cache(new Sharding($adapters)); } $realtime = new Realtime(); From 5657e7d04ee852422d7c15868481da2a551dabf4 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 28 Mar 2024 13:08:55 +0200 Subject: [PATCH 23/61] Bust database 45-8 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index eaa6cfa206..64acc874b1 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cache": "0.9.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.45.7", + "utopia-php/database": "0.45.8", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.1.*", "utopia-php/framework": "0.33.*", From 5ff5c930a5a9a3faf98217659a89a35379171bca Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 28 Mar 2024 13:32:54 +0200 Subject: [PATCH 24/61] composer.lock --- composer.lock | 52 +++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/composer.lock b/composer.lock index 606338c675..c1ef452b4d 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "95d4c9ccd4b2f958ca247d83b04d1391", + "content-hash": "1a272e08dfe04dbb86b65a5e1c419bb3", "packages": [ { "name": "adhocore/jwt", @@ -1041,16 +1041,16 @@ }, { "name": "utopia-php/cache", - "version": "0.9.0", + "version": "0.9.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "4fc7b4789b5f0ce74835c1ecfec4f3afe6f0e34e" + "reference": "552b4c554bb14d0c529631ce304cdf4a2b9d06a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/4fc7b4789b5f0ce74835c1ecfec4f3afe6f0e34e", - "reference": "4fc7b4789b5f0ce74835c1ecfec4f3afe6f0e34e", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/552b4c554bb14d0c529631ce304cdf4a2b9d06a6", + "reference": "552b4c554bb14d0c529631ce304cdf4a2b9d06a6", "shasum": "" }, "require": { @@ -1085,9 +1085,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.9.0" + "source": "https://github.com/utopia-php/cache/tree/0.9.1" }, - "time": "2024-01-07T18:11:23+00:00" + "time": "2024-03-19T17:07:20+00:00" }, { "name": "utopia-php/cli", @@ -1191,16 +1191,16 @@ }, { "name": "utopia-php/database", - "version": "0.45.7", + "version": "0.45.8", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "3ffcc226cbc5b8747cdc5d6b06ce887cdffee8e1" + "reference": "ca1f6b0c4edf4be1db42788df953682f921565cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/3ffcc226cbc5b8747cdc5d6b06ce887cdffee8e1", - "reference": "3ffcc226cbc5b8747cdc5d6b06ce887cdffee8e1", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ca1f6b0c4edf4be1db42788df953682f921565cb", + "reference": "ca1f6b0c4edf4be1db42788df953682f921565cb", "shasum": "" }, "require": { @@ -1208,7 +1208,7 @@ "ext-pdo": "*", "php": ">=8.0", "utopia-php/cache": "0.9.*", - "utopia-php/framework": "0.*.*", + "utopia-php/framework": "0.33.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { @@ -1241,9 +1241,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.45.7" + "source": "https://github.com/utopia-php/database/tree/0.45.8" }, - "time": "2024-03-13T18:03:15+00:00" + "time": "2024-03-28T10:52:53+00:00" }, { "name": "utopia-php/domains", @@ -1393,16 +1393,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.3", + "version": "0.33.6", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "893d602cd96676810c25fc9b9a2e9360eebb898f" + "reference": "8fe57da0cecd57e3b17cd395b4a666a24f4c07a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/893d602cd96676810c25fc9b9a2e9360eebb898f", - "reference": "893d602cd96676810c25fc9b9a2e9360eebb898f", + "url": "https://api.github.com/repos/utopia-php/http/zipball/8fe57da0cecd57e3b17cd395b4a666a24f4c07a6", + "reference": "8fe57da0cecd57e3b17cd395b4a666a24f4c07a6", "shasum": "" }, "require": { @@ -1432,9 +1432,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.3" + "source": "https://github.com/utopia-php/http/tree/0.33.6" }, - "time": "2024-03-11T13:43:23+00:00" + "time": "2024-03-21T18:10:57+00:00" }, { "name": "utopia-php/image", @@ -3220,16 +3220,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.26.0", + "version": "1.27.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "231e3186624c03d7e7c890ec662b81e6b0405227" + "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/231e3186624c03d7e7c890ec662b81e6b0405227", - "reference": "231e3186624c03d7e7c890ec662b81e6b0405227", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/86e4d5a4b036f8f0be1464522f4c6b584c452757", + "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757", "shasum": "" }, "require": { @@ -3261,9 +3261,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.26.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.27.0" }, - "time": "2024-02-23T16:05:55+00:00" + "time": "2024-03-21T13:14:53+00:00" }, { "name": "phpunit/php-code-coverage", From 803ba6182cf2ae2d24554ec12aa0e9bf81fb3448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 29 Mar 2024 10:48:31 +0100 Subject: [PATCH 25/61] Update composer.lock --- composer.lock | 351 ++++++++++++++++++++++++++++---------------------- 1 file changed, 198 insertions(+), 153 deletions(-) diff --git a/composer.lock b/composer.lock index ce4c6afca8..d38e1baff5 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "35dcde03d0eb9a0d27de653b9fa038d4", + "content-hash": "1a272e08dfe04dbb86b65a5e1c419bb3", "packages": [ { "name": "adhocore/jwt", @@ -156,11 +156,11 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.13.2", + "version": "0.13.3", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "214a37c2c66e0f2bc9c30fdfde66955d9fd084a1" + "reference": "5d93fc578a9a543bcdc9b2c0562d80a51d56c73d" }, "require": { "php": ">=8.0", @@ -195,7 +195,7 @@ "php", "runtimes" ], - "time": "2023-11-22T15:36:00+00:00" + "time": "2024-03-01T14:47:47+00:00" }, { "name": "chillerlan/php-qrcode", @@ -404,16 +404,16 @@ }, { "name": "jean85/pretty-package-versions", - "version": "2.0.5", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", "shasum": "" }, "require": { @@ -421,9 +421,9 @@ "php": "^7.1|^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.17", + "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^0.12.66", + "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^7.5|^8.5|^9.4", "vimeo/psalm": "^4.3" }, @@ -457,9 +457,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" }, - "time": "2021-10-08T21:21:46+00:00" + "time": "2024-03-08T09:58:59+00:00" }, { "name": "league/csv", @@ -688,7 +688,7 @@ "version": "0.6.3", "source": { "type": "git", - "url": "git@github.com:mustangostang/spyc.git", + "url": "https://github.com/mustangostang/spyc.git", "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0" }, "dist": { @@ -731,6 +731,10 @@ "yaml", "yml" ], + "support": { + "issues": "https://github.com/mustangostang/spyc/issues", + "source": "https://github.com/mustangostang/spyc/tree/0.6.3" + }, "time": "2019-09-10T13:16:29+00:00" }, { @@ -1037,16 +1041,16 @@ }, { "name": "utopia-php/cache", - "version": "0.9.0", + "version": "0.9.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "4fc7b4789b5f0ce74835c1ecfec4f3afe6f0e34e" + "reference": "552b4c554bb14d0c529631ce304cdf4a2b9d06a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/4fc7b4789b5f0ce74835c1ecfec4f3afe6f0e34e", - "reference": "4fc7b4789b5f0ce74835c1ecfec4f3afe6f0e34e", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/552b4c554bb14d0c529631ce304cdf4a2b9d06a6", + "reference": "552b4c554bb14d0c529631ce304cdf4a2b9d06a6", "shasum": "" }, "require": { @@ -1081,9 +1085,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.9.0" + "source": "https://github.com/utopia-php/cache/tree/0.9.1" }, - "time": "2024-01-07T18:11:23+00:00" + "time": "2024-03-19T17:07:20+00:00" }, { "name": "utopia-php/cli", @@ -1187,16 +1191,16 @@ }, { "name": "utopia-php/database", - "version": "0.45.6", + "version": "0.45.8", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "c7cc6d57683a4c13d9772dbeea343adb72c443fd" + "reference": "ca1f6b0c4edf4be1db42788df953682f921565cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/c7cc6d57683a4c13d9772dbeea343adb72c443fd", - "reference": "c7cc6d57683a4c13d9772dbeea343adb72c443fd", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ca1f6b0c4edf4be1db42788df953682f921565cb", + "reference": "ca1f6b0c4edf4be1db42788df953682f921565cb", "shasum": "" }, "require": { @@ -1204,7 +1208,7 @@ "ext-pdo": "*", "php": ">=8.0", "utopia-php/cache": "0.9.*", - "utopia-php/framework": "0.*.*", + "utopia-php/framework": "0.33.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { @@ -1237,22 +1241,22 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.45.6" + "source": "https://github.com/utopia-php/database/tree/0.45.8" }, - "time": "2024-02-01T02:33:43+00:00" + "time": "2024-03-28T10:52:53+00:00" }, { "name": "utopia-php/domains", - "version": "0.3.2", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/utopia-php/domains.git", - "reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb" + "reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/aaa8c9a96c69ccb397997b1f4f2299c66f77eefb", - "reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/bf07f60326f8389f378ddf6fcde86217e5cfe18c", + "reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c", "shasum": "" }, "require": { @@ -1297,9 +1301,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/0.3.2" + "source": "https://github.com/utopia-php/domains/tree/0.5.0" }, - "time": "2023-07-19T16:39:24+00:00" + "time": "2024-01-03T22:04:27+00:00" }, { "name": "utopia-php/dsn", @@ -1349,17 +1353,56 @@ "time": "2022-10-26T10:06:20+00:00" }, { - "name": "utopia-php/framework", - "version": "0.33.2", + "name": "utopia-php/fetch", + "version": "0.2.1", "source": { "type": "git", - "url": "https://github.com/utopia-php/http.git", - "reference": "b1423ca3e3b61c6c4c2e619d2cb80672809a19f3" + "url": "https://github.com/utopia-php/fetch.git", + "reference": "1423c0ee3eef944d816ca6e31706895b585aea82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/b1423ca3e3b61c6c4c2e619d2cb80672809a19f3", - "reference": "b1423ca3e3b61c6c4c2e619d2cb80672809a19f3", + "url": "https://api.github.com/repos/utopia-php/fetch/zipball/1423c0ee3eef944d816ca6e31706895b585aea82", + "reference": "1423c0ee3eef944d816ca6e31706895b585aea82", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "^1.5.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Fetch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple library that provides an interface for making HTTP Requests.", + "support": { + "issues": "https://github.com/utopia-php/fetch/issues", + "source": "https://github.com/utopia-php/fetch/tree/0.2.1" + }, + "time": "2024-03-18T11:50:59+00:00" + }, + { + "name": "utopia-php/framework", + "version": "0.33.6", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/http.git", + "reference": "8fe57da0cecd57e3b17cd395b4a666a24f4c07a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/http/zipball/8fe57da0cecd57e3b17cd395b4a666a24f4c07a6", + "reference": "8fe57da0cecd57e3b17cd395b4a666a24f4c07a6", "shasum": "" }, "require": { @@ -1389,22 +1432,22 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.2" + "source": "https://github.com/utopia-php/http/tree/0.33.6" }, - "time": "2024-01-31T10:35:59+00:00" + "time": "2024-03-21T18:10:57+00:00" }, { "name": "utopia-php/image", - "version": "0.5.4", + "version": "0.6.1", "source": { "type": "git", "url": "https://github.com/utopia-php/image.git", - "reference": "ca5f436f9aa22dedaa6648f24f3687733808e336" + "reference": "2d74c27e69e65a93cf94a16586598a04fe435bf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/image/zipball/ca5f436f9aa22dedaa6648f24f3687733808e336", - "reference": "ca5f436f9aa22dedaa6648f24f3687733808e336", + "url": "https://api.github.com/repos/utopia-php/image/zipball/2d74c27e69e65a93cf94a16586598a04fe435bf0", + "reference": "2d74c27e69e65a93cf94a16586598a04fe435bf0", "shasum": "" }, "require": { @@ -1412,6 +1455,8 @@ "php": ">=8.0" }, "require-dev": { + "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.9.x-dev", "phpunit/phpunit": "^9.3", "vimeo/psalm": "4.13.1" }, @@ -1425,12 +1470,6 @@ "license": [ "MIT" ], - "authors": [ - { - "name": "Eldad Fux", - "email": "eldad@appwrite.io" - } - ], "description": "A simple Image manipulation library", "keywords": [ "framework", @@ -1441,9 +1480,9 @@ ], "support": { "issues": "https://github.com/utopia-php/image/issues", - "source": "https://github.com/utopia-php/image/tree/0.5.4" + "source": "https://github.com/utopia-php/image/tree/0.6.1" }, - "time": "2022-05-11T12:30:41+00:00" + "time": "2024-02-05T13:31:44+00:00" }, { "name": "utopia-php/locale", @@ -1551,16 +1590,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.2.0", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43" + "reference": "d488223876f88f97bb76fd6681fed0df80558f62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/2d0f474a106bb1da285f85e105c29b46085d3a43", - "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/d488223876f88f97bb76fd6681fed0df80558f62", + "reference": "d488223876f88f97bb76fd6681fed0df80558f62", "shasum": "" }, "require": { @@ -1593,9 +1632,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.2.0" + "source": "https://github.com/utopia-php/messaging/tree/0.3.0" }, - "time": "2023-09-14T20:48:42+00:00" + "time": "2023-11-14T21:02:37+00:00" }, { "name": "utopia-php/migration", @@ -2417,16 +2456,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.36.2", + "version": "0.36.4", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "0aa67479d75f0e0cb7b60454031534d7f0abaece" + "reference": "8d932098009d62d37dda73cfe4ebc11f83e21405" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/0aa67479d75f0e0cb7b60454031534d7f0abaece", - "reference": "0aa67479d75f0e0cb7b60454031534d7f0abaece", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/8d932098009d62d37dda73cfe4ebc11f83e21405", + "reference": "8d932098009d62d37dda73cfe4ebc11f83e21405", "shasum": "" }, "require": { @@ -2462,9 +2501,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.36.2" + "source": "https://github.com/appwrite/sdk-generator/tree/0.36.4" }, - "time": "2024-01-19T01:04:35+00:00" + "time": "2024-02-20T16:36:15+00:00" }, { "name": "doctrine/deprecations", @@ -2585,16 +2624,16 @@ }, { "name": "matthiasmullie/minify", - "version": "1.3.71", + "version": "1.3.73", "source": { "type": "git", "url": "https://github.com/matthiasmullie/minify.git", - "reference": "ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1" + "reference": "cb7a9297b4ab070909cefade30ee95054d4ae87a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1", - "reference": "ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1", + "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/cb7a9297b4ab070909cefade30ee95054d4ae87a", + "reference": "cb7a9297b4ab070909cefade30ee95054d4ae87a", "shasum": "" }, "require": { @@ -2644,7 +2683,7 @@ ], "support": { "issues": "https://github.com/matthiasmullie/minify/issues", - "source": "https://github.com/matthiasmullie/minify/tree/1.3.71" + "source": "https://github.com/matthiasmullie/minify/tree/1.3.73" }, "funding": [ { @@ -2652,7 +2691,7 @@ "type": "github" } ], - "time": "2023-04-25T20:33:03+00:00" + "time": "2024-03-15T10:27:10+00:00" }, { "name": "matthiasmullie/path-converter", @@ -2768,16 +2807,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.0.0", + "version": "v5.0.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc" + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc", - "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", "shasum": "" }, "require": { @@ -2820,26 +2859,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" }, - "time": "2024-01-07T17:17:35+00:00" + "time": "2024-03-05T20:51:40+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -2880,9 +2920,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -3047,21 +3093,21 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.8.0", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc" + "reference": "153ae662783729388a584b4361f2545e4d841e3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fad452781b3d774e3337b0c0b245dd8e5a4455fc", - "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", + "reference": "153ae662783729388a584b4361f2545e4d841e3c", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", - "php": "^7.4 || ^8.0", + "php": "^7.3 || ^8.0", "phpdocumentor/reflection-common": "^2.0", "phpstan/phpdoc-parser": "^1.13" }, @@ -3099,30 +3145,30 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" }, - "time": "2024-01-11T11:49:22+00:00" + "time": "2024-02-23T11:10:43+00:00" }, { "name": "phpspec/prophecy", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "d4f454f7e1193933f04e6500de3e79191648ed0c" + "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d4f454f7e1193933f04e6500de3e79191648ed0c", - "reference": "d4f454f7e1193933f04e6500de3e79191648ed0c", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/67a759e7d8746d501c41536ba40cd9c0a07d6a87", + "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2 || ^2.0", "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.*", "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0 || ^5.0", - "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0" + "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0", + "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0" }, "require-dev": { "phpspec/phpspec": "^6.0 || ^7.0", @@ -3168,22 +3214,22 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.18.0" + "source": "https://github.com/phpspec/prophecy/tree/v1.19.0" }, - "time": "2023-12-07T16:22:33+00:00" + "time": "2024-02-29T11:52:51+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.25.0", + "version": "1.27.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" + "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/86e4d5a4b036f8f0be1464522f4c6b584c452757", + "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757", "shasum": "" }, "require": { @@ -3215,22 +3261,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.27.0" }, - "time": "2024-01-04T17:06:16+00:00" + "time": "2024-03-21T13:14:53+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.30", + "version": "9.2.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { @@ -3287,7 +3333,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" }, "funding": [ { @@ -3295,7 +3341,7 @@ "type": "github" } ], - "time": "2023-12-22T06:47:57+00:00" + "time": "2024-03-02T06:37:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3693,16 +3739,16 @@ }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -3737,7 +3783,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -3745,7 +3791,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -3991,16 +4037,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -4045,7 +4091,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -4053,7 +4099,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -4120,16 +4166,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -4185,7 +4231,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -4193,20 +4239,20 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -4249,7 +4295,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -4257,7 +4303,7 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", @@ -4493,16 +4539,16 @@ }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -4514,7 +4560,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -4535,8 +4581,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -4544,7 +4589,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -4657,16 +4702,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.8.1", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7" + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/14f5fff1e64118595db5408e946f3a22c75807f7", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", "shasum": "" }, "require": { @@ -4733,7 +4778,7 @@ "type": "open_collective" } ], - "time": "2024-01-11T20:47:48+00:00" + "time": "2024-02-16T15:06:51+00:00" }, { "name": "swoole/ide-helper", @@ -4977,16 +5022,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -5015,7 +5060,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -5023,7 +5068,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { "name": "twig/twig", @@ -5124,5 +5169,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } From f8c44c8a501aff02e42c8bece42f42afe3a0fb7b Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Fri, 29 Mar 2024 10:18:33 +0000 Subject: [PATCH 26/61] chore: linter --- app/realtime.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index c97eb598b8..e4c2f38dc5 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -37,7 +37,7 @@ require_once __DIR__ . '/init.php'; Runtime::enableCoroutine(SWOOLE_HOOK_ALL); // Allows overriding -if(!function_exists("getConsoleDB")) { +if (!function_exists("getConsoleDB")) { function getConsoleDB(): Database { global $register; @@ -63,7 +63,7 @@ if(!function_exists("getConsoleDB")) { } // Allows overriding -if(!function_exists("getProjectDB")) { +if (!function_exists("getProjectDB")) { function getProjectDB(Document $project): Database { global $register; @@ -93,7 +93,7 @@ if(!function_exists("getProjectDB")) { } // Allows overriding -if(!function_exists("getCache")) { +if (!function_exists("getCache")) { function getCache(): Cache { global $register; From 5ee5ffb288df59999571502bda29831d274d5bf4 Mon Sep 17 00:00:00 2001 From: Steven Nguyen <1477010+stnguyen90@users.noreply.github.com> Date: Fri, 29 Mar 2024 21:24:05 +0000 Subject: [PATCH 27/61] refactor: remove var_dump calls --- app/controllers/general.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 13a86ac36b..434117846b 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -820,31 +820,24 @@ App::error() } break; case 'Utopia\Database\Exception\Conflict': - \var_dump('Wrapping conflict exception'); $error = new AppwriteException(AppwriteException::DOCUMENT_UPDATE_CONFLICT, previous: $error); break; case 'Utopia\Database\Exception\Timeout': - \var_dump('Wrapping timeout exception'); $error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error); break; case 'Utopia\Database\Exception\Query': - \var_dump('Wrapping query exception'); $error = new AppwriteException(AppwriteException::GENERAL_QUERY_INVALID, $error->getMessage(), previous: $error); break; case 'Utopia\Database\Exception\Structure': - \var_dump('Wrapping structure exception'); $error = new AppwriteException(AppwriteException::DOCUMENT_INVALID_STRUCTURE, $error->getMessage(), previous: $error); break; case 'Utopia\Database\Exception\Duplicate': - \var_dump('Wrapping duplicate exception'); $error = new AppwriteException(AppwriteException::DOCUMENT_ALREADY_EXISTS); break; case 'Utopia\Database\Exception\Restricted': - \var_dump('Wrapping restricted exception'); $error = new AppwriteException(AppwriteException::DOCUMENT_DELETE_RESTRICTED); break; case 'Utopia\Database\Exception\Authorization': - \var_dump('Wrapping authorization exception'); $error = new AppwriteException(AppwriteException::USER_UNAUTHORIZED); break; } From 75d8cc3327838047bbcf671d778e6cc305534a37 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 1 Apr 2024 13:27:44 +0530 Subject: [PATCH 28/61] Updated error code for OAuth error --- app/config/errors.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/errors.php b/app/config/errors.php index 1699157d8c..91698e1f78 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -639,7 +639,7 @@ return [ Exception::PROJECT_PROVIDER_UNSUPPORTED => [ 'name' => Exception::PROJECT_PROVIDER_UNSUPPORTED, 'description' => 'The chosen OAuth provider is unsupported. Please check the Create OAuth2 Session docs for the complete list of supported OAuth providers.', - 'code' => 501, + 'code' => 400, ], Exception::PROJECT_INVALID_SUCCESS_URL => [ 'name' => Exception::PROJECT_INVALID_SUCCESS_URL, From eb8543c6907f660c8ea8f9fa4f54e33a0342a63d Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:04:36 +0530 Subject: [PATCH 29/61] Create failed execution if deployment doesn't exist --- src/Appwrite/Platform/Workers/Functions.php | 58 ++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index bde5644ade..e7f28a8dcb 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -194,6 +194,51 @@ class Functions extends Action } } + /** + * @param Document $function + * @param string $trigger + * @param string $path + * @param string $method + * @param Document $user + * @throws Exception + */ + private function fail( + Database $dbForProject, + Document $function, + string $trigger, + string $path, + string $method, + Document $user, + ): void { + $executionId = ID::unique(); + $execution = new Document([ + '$id' => $executionId, + '$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))], + 'functionInternalId' => $function->getInternalId(), + 'functionId' => $function->getId(), + 'deploymentInternalId' => '', + 'deploymentId' => '', + 'trigger' => $trigger, + 'status' => 'failed', + 'responseStatusCode' => 400, + 'responseHeaders' => [], + 'requestPath' => $path, + 'requestMethod' => $method, + 'errors' => 'Deployment not found. Create deployment before trying to execute a function.', + 'logs' => '', + 'duration' => 0.0, + 'search' => implode(' ', [$function->getId(), $executionId]), + ]); + + if ($function->getAttribute('logging')) { + $execution = $dbForProject->createDocument('executions', $execution); + } + + if ($execution->isEmpty()) { + throw new Exception('Failed to create execution'); + } + } + /** * @param Log $log * @param Database $dbForProject @@ -248,15 +293,17 @@ class Functions extends Action $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->getAttribute('resourceId') !== $functionId) { - throw new Exception('Deployment not found. Create deployment before trying to execute a function'); + $this->fail($dbForProject, $function, $trigger, $path, $method, $user); + return; } if ($deployment->isEmpty()) { - throw new Exception('Deployment not found. Create deployment before trying to execute a function'); + $this->fail($dbForProject, $function, $trigger, $path, $method, $user); + return; } - /** Check if build has exists */ - $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); + /** Check if the build exists */ + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); if ($build->isEmpty()) { throw new Exception('Build not found'); } @@ -265,7 +312,7 @@ class Functions extends Action throw new Exception('Build not ready'); } - /** Check if runtime is supported */ + /** Check if runtime is supported */ $version = $function->getAttribute('version', 'v2'); $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); @@ -280,7 +327,6 @@ class Functions extends Action $headers['x-appwrite-user-id'] = $user->getId() ?? ''; $headers['x-appwrite-user-jwt'] = $jwt ?? ''; - /** Create execution or update execution status */ /** Create execution or update execution status */ $execution = $dbForProject->getDocument('executions', $executionId ?? ''); if ($execution->isEmpty()) { From ec930bf125be181e09df4248793ca69f2d91d8a9 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:18:42 +0530 Subject: [PATCH 30/61] Add request headers --- src/Appwrite/Platform/Workers/Functions.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index e7f28a8dcb..8ed69987e5 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -200,6 +200,8 @@ class Functions extends Action * @param string $path * @param string $method * @param Document $user + * @param string|null $jwt + * @param string|null $event * @throws Exception */ private function fail( @@ -209,7 +211,21 @@ class Functions extends Action string $path, string $method, Document $user, + string $jwt = null, + string $event = null, ): void { + $headers['x-appwrite-trigger'] = $trigger; + $headers['x-appwrite-event'] = $event ?? ''; + $headers['x-appwrite-user-id'] = $user->getId() ?? ''; + $headers['x-appwrite-user-jwt'] = $jwt ?? ''; + + $headersFiltered = []; + foreach ($headers as $key => $value) { + if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) { + $headersFiltered[] = ['name' => $key, 'value' => $value]; + } + } + $executionId = ID::unique(); $execution = new Document([ '$id' => $executionId, @@ -224,6 +240,7 @@ class Functions extends Action 'responseHeaders' => [], 'requestPath' => $path, 'requestMethod' => $method, + 'requestHeaders' => $headersFiltered, 'errors' => 'Deployment not found. Create deployment before trying to execute a function.', 'logs' => '', 'duration' => 0.0, @@ -293,12 +310,12 @@ class Functions extends Action $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->getAttribute('resourceId') !== $functionId) { - $this->fail($dbForProject, $function, $trigger, $path, $method, $user); + $this->fail($dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event); return; } if ($deployment->isEmpty()) { - $this->fail($dbForProject, $function, $trigger, $path, $method, $user); + $this->fail($dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event); return; } From a3a7763340643f3e9768216d3e966699066442c7 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 1 Apr 2024 19:07:17 +0530 Subject: [PATCH 31/61] Created failed execution for build errors --- src/Appwrite/Platform/Workers/Functions.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 8ed69987e5..1a86579662 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -195,6 +195,7 @@ class Functions extends Action } /** + * @param string $message * @param Document $function * @param string $trigger * @param string $path @@ -205,6 +206,7 @@ class Functions extends Action * @throws Exception */ private function fail( + string $message, Database $dbForProject, Document $function, string $trigger, @@ -241,7 +243,7 @@ class Functions extends Action 'requestPath' => $path, 'requestMethod' => $method, 'requestHeaders' => $headersFiltered, - 'errors' => 'Deployment not found. Create deployment before trying to execute a function.', + 'errors' => $message, 'logs' => '', 'duration' => 0.0, 'search' => implode(' ', [$function->getId(), $executionId]), @@ -310,23 +312,29 @@ class Functions extends Action $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->getAttribute('resourceId') !== $functionId) { - $this->fail($dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event); + $errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.'; + $this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event); return; } if ($deployment->isEmpty()) { - $this->fail($dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event); + $errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.'; + $this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event); return; } /** Check if the build exists */ $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); if ($build->isEmpty()) { - throw new Exception('Build not found'); + $errorMessage = 'The execution could not be completed because a corresponding build was not found. A function build needs to be created before it can be executed. Please create a build for your function and try again.'; + $this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event); + return; } if ($build->getAttribute('status') !== 'ready') { - throw new Exception('Build not ready'); + $errorMessage = 'The execution could not be completed because the build is not ready. Please wait for the build to complete and try again.'; + $this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event); + return; } /** Check if runtime is supported */ From e7e85bfb764d7d47d5858c630a06526a1b062c71 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 1 Apr 2024 19:37:51 +0530 Subject: [PATCH 32/61] Update response code --- src/Appwrite/Platform/Workers/Functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 1a86579662..2ae644a24e 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -238,7 +238,7 @@ class Functions extends Action 'deploymentId' => '', 'trigger' => $trigger, 'status' => 'failed', - 'responseStatusCode' => 400, + 'responseStatusCode' => 0, 'responseHeaders' => [], 'requestPath' => $path, 'requestMethod' => $method, @@ -326,7 +326,7 @@ class Functions extends Action /** Check if the build exists */ $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); if ($build->isEmpty()) { - $errorMessage = 'The execution could not be completed because a corresponding build was not found. A function build needs to be created before it can be executed. Please create a build for your function and try again.'; + $errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.'; $this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event); return; } From 3469ebf7b046724615f7e16687606cd5ccf306cd Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:07:11 +0530 Subject: [PATCH 33/61] Add var_dump to debug flaky test --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index e39c6573e4..56e1c24bd3 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1069,6 +1069,8 @@ class FunctionsCustomServerTest extends Scope 'async' => false ]); + var_dump($execution['body']); + $executionId = $execution['body']['$id'] ?? ''; $output = json_decode($execution['body']['responseBody'], true); From 39da1b8028cd2ee9992574f37dab08cdb5690263 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 2 Apr 2024 08:32:30 +0000 Subject: [PATCH 34/61] composer update --- composer.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/composer.lock b/composer.lock index d38e1baff5..caad1b4090 100644 --- a/composer.lock +++ b/composer.lock @@ -156,15 +156,15 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.13.3", + "version": "0.13.5", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "5d93fc578a9a543bcdc9b2c0562d80a51d56c73d" + "reference": "ba24c3a163f1a1da6cd355db92def508d05e59f7" }, "require": { "php": ">=8.0", - "utopia-php/system": "0.7.*" + "utopia-php/system": "0.8.*" }, "require-dev": { "phpunit/phpunit": "^9.3", @@ -195,7 +195,7 @@ "php", "runtimes" ], - "time": "2024-03-01T14:47:47+00:00" + "time": "2024-04-01T10:35:02+00:00" }, { "name": "chillerlan/php-qrcode", @@ -2062,16 +2062,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.3", + "version": "0.18.4", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "faa0279519ac14f3501e8b138e0865ad9d12bff6" + "reference": "94ab8758fabcefee5c5fa723616e45719833f922" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/faa0279519ac14f3501e8b138e0865ad9d12bff6", - "reference": "faa0279519ac14f3501e8b138e0865ad9d12bff6", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/94ab8758fabcefee5c5fa723616e45719833f922", + "reference": "94ab8758fabcefee5c5fa723616e45719833f922", "shasum": "" }, "require": { @@ -2111,9 +2111,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.3" + "source": "https://github.com/utopia-php/storage/tree/0.18.4" }, - "time": "2023-12-31T11:45:12+00:00" + "time": "2024-04-02T08:24:09+00:00" }, { "name": "utopia-php/swoole", @@ -2168,16 +2168,16 @@ }, { "name": "utopia-php/system", - "version": "0.7.2", + "version": "0.8.0", "source": { "type": "git", "url": "https://github.com/utopia-php/system.git", - "reference": "4593d4d334b0c15879c4744a826e0362924c5d66" + "reference": "a2cbfb3c69b9ecb8b6f06c5774f3cf279ea7665e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/system/zipball/4593d4d334b0c15879c4744a826e0362924c5d66", - "reference": "4593d4d334b0c15879c4744a826e0362924c5d66", + "url": "https://api.github.com/repos/utopia-php/system/zipball/a2cbfb3c69b9ecb8b6f06c5774f3cf279ea7665e", + "reference": "a2cbfb3c69b9ecb8b6f06c5774f3cf279ea7665e", "shasum": "" }, "require": { @@ -2218,9 +2218,9 @@ ], "support": { "issues": "https://github.com/utopia-php/system/issues", - "source": "https://github.com/utopia-php/system/tree/0.7.2" + "source": "https://github.com/utopia-php/system/tree/0.8.0" }, - "time": "2023-10-20T01:39:17+00:00" + "time": "2024-04-01T10:22:28+00:00" }, { "name": "utopia-php/vcs", @@ -4702,16 +4702,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.0", + "version": "3.9.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" + "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/267a4405fff1d9c847134db3a3c92f1ab7f77909", + "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909", "shasum": "" }, "require": { @@ -4778,7 +4778,7 @@ "type": "open_collective" } ], - "time": "2024-02-16T15:06:51+00:00" + "time": "2024-03-31T21:03:09+00:00" }, { "name": "swoole/ide-helper", @@ -5169,5 +5169,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } From 026f5dde2d642ad4a8438524a2e1a3b1824833ba Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 2 Apr 2024 08:35:14 +0000 Subject: [PATCH 35/61] set http version to 1.1 for do spaces --- app/init.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/init.php b/app/init.php index cd97fba276..e79811c42d 100644 --- a/app/init.php +++ b/app/init.php @@ -1266,7 +1266,9 @@ function getDevice($root): Device case Storage::DEVICE_S3: return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl); case STORAGE::DEVICE_DO_SPACES: - return new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl); + $device = new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl); + $device->setHttpVersion(S3::HTTP_VERSION_1_1); + return $device; case Storage::DEVICE_BACKBLAZE: return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl); case Storage::DEVICE_LINODE: @@ -1295,7 +1297,9 @@ function getDevice($root): Device $doSpacesRegion = App::getEnv('_APP_STORAGE_DO_SPACES_REGION', ''); $doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', ''); $doSpacesAcl = 'private'; - return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl); + $device = new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl); + $device->setHttpVersion(S3::HTTP_VERSION_1_1); + return $device; case Storage::DEVICE_BACKBLAZE: $backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', ''); $backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', ''); From 7a25a3c529b1633108d130ba3c4a59781d20f1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 2 Apr 2024 11:42:10 +0200 Subject: [PATCH 36/61] Upgrade database lib for ZDT --- composer.json | 2 +- composer.lock | 58 +++++++++++++++++++++++++-------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/composer.json b/composer.json index 64acc874b1..90e727bcac 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cache": "0.9.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.45.8", + "utopia-php/database": "0.45.*", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.1.*", "utopia-php/framework": "0.33.*", diff --git a/composer.lock b/composer.lock index c1ef452b4d..6b773cb132 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "1a272e08dfe04dbb86b65a5e1c419bb3", + "content-hash": "bbf819232382a6aeabe9d763844c3188", "packages": [ { "name": "adhocore/jwt", @@ -156,15 +156,15 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.13.3", + "version": "0.13.5", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "5d93fc578a9a543bcdc9b2c0562d80a51d56c73d" + "reference": "ba24c3a163f1a1da6cd355db92def508d05e59f7" }, "require": { "php": ">=8.0", - "utopia-php/system": "0.7.*" + "utopia-php/system": "0.8.*" }, "require-dev": { "phpunit/phpunit": "^9.3", @@ -195,7 +195,7 @@ "php", "runtimes" ], - "time": "2024-03-01T14:47:47+00:00" + "time": "2024-04-01T10:35:02+00:00" }, { "name": "chillerlan/php-qrcode", @@ -1191,16 +1191,16 @@ }, { "name": "utopia-php/database", - "version": "0.45.8", + "version": "0.45.9", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "ca1f6b0c4edf4be1db42788df953682f921565cb" + "reference": "e43e4b42f2a43e56dcfdc99ca527dfc885ca0667" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/ca1f6b0c4edf4be1db42788df953682f921565cb", - "reference": "ca1f6b0c4edf4be1db42788df953682f921565cb", + "url": "https://api.github.com/repos/utopia-php/database/zipball/e43e4b42f2a43e56dcfdc99ca527dfc885ca0667", + "reference": "e43e4b42f2a43e56dcfdc99ca527dfc885ca0667", "shasum": "" }, "require": { @@ -1241,9 +1241,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.45.8" + "source": "https://github.com/utopia-php/database/tree/0.45.9" }, - "time": "2024-03-28T10:52:53+00:00" + "time": "2024-04-02T09:22:05+00:00" }, { "name": "utopia-php/domains", @@ -2062,16 +2062,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.3", + "version": "0.18.4", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "faa0279519ac14f3501e8b138e0865ad9d12bff6" + "reference": "94ab8758fabcefee5c5fa723616e45719833f922" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/faa0279519ac14f3501e8b138e0865ad9d12bff6", - "reference": "faa0279519ac14f3501e8b138e0865ad9d12bff6", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/94ab8758fabcefee5c5fa723616e45719833f922", + "reference": "94ab8758fabcefee5c5fa723616e45719833f922", "shasum": "" }, "require": { @@ -2111,9 +2111,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.3" + "source": "https://github.com/utopia-php/storage/tree/0.18.4" }, - "time": "2023-12-31T11:45:12+00:00" + "time": "2024-04-02T08:24:09+00:00" }, { "name": "utopia-php/swoole", @@ -2168,16 +2168,16 @@ }, { "name": "utopia-php/system", - "version": "0.7.2", + "version": "0.8.0", "source": { "type": "git", "url": "https://github.com/utopia-php/system.git", - "reference": "4593d4d334b0c15879c4744a826e0362924c5d66" + "reference": "a2cbfb3c69b9ecb8b6f06c5774f3cf279ea7665e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/system/zipball/4593d4d334b0c15879c4744a826e0362924c5d66", - "reference": "4593d4d334b0c15879c4744a826e0362924c5d66", + "url": "https://api.github.com/repos/utopia-php/system/zipball/a2cbfb3c69b9ecb8b6f06c5774f3cf279ea7665e", + "reference": "a2cbfb3c69b9ecb8b6f06c5774f3cf279ea7665e", "shasum": "" }, "require": { @@ -2218,9 +2218,9 @@ ], "support": { "issues": "https://github.com/utopia-php/system/issues", - "source": "https://github.com/utopia-php/system/tree/0.7.2" + "source": "https://github.com/utopia-php/system/tree/0.8.0" }, - "time": "2023-10-20T01:39:17+00:00" + "time": "2024-04-01T10:22:28+00:00" }, { "name": "utopia-php/vcs", @@ -4702,16 +4702,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.0", + "version": "3.9.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" + "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/267a4405fff1d9c847134db3a3c92f1ab7f77909", + "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909", "shasum": "" }, "require": { @@ -4778,7 +4778,7 @@ "type": "open_collective" } ], - "time": "2024-02-16T15:06:51+00:00" + "time": "2024-03-31T21:03:09+00:00" }, { "name": "swoole/ide-helper", @@ -5169,5 +5169,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } From 7c691b3c7c6336ff70e59a44daa258f615e2cdfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 2 Apr 2024 13:01:55 +0200 Subject: [PATCH 37/61] Fix version missmatch --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 90e727bcac..ca7a350ebc 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cache": "0.9.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.45.*", + "utopia-php/database": "0.45.9", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.1.*", "utopia-php/framework": "0.33.*", diff --git a/composer.lock b/composer.lock index 6b773cb132..742c6ff80c 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "bbf819232382a6aeabe9d763844c3188", + "content-hash": "fca40e29f72e83282e4c492c3a81674c", "packages": [ { "name": "adhocore/jwt", From 26db6fa4c51657a312bf181260c6b10e4e25e32b Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:49:09 +0530 Subject: [PATCH 38/61] Remove var-dump --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 56e1c24bd3..e39c6573e4 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1069,8 +1069,6 @@ class FunctionsCustomServerTest extends Scope 'async' => false ]); - var_dump($execution['body']); - $executionId = $execution['body']['$id'] ?? ''; $output = json_decode($execution['body']['responseBody'], true); From 008c3ac19add0770d20b0c80f8a6b83932fec975 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:18:15 +0530 Subject: [PATCH 39/61] Update executor version --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 60eef4989e..a4daa80e8c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -751,7 +751,7 @@ services: hostname: appwrite-executor <<: *x-logging stop_signal: SIGINT - image: openruntimes/executor:0.4.5 + image: openruntimes/executor:0.5.0 restart: unless-stopped networks: - appwrite From f66a65f27bd50754554dbe939461825e7689c76e Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 3 Apr 2024 22:54:14 +0530 Subject: [PATCH 40/61] Comment timer tick --- app/realtime.php | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index e4c2f38dc5..9c6ae850e4 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -213,29 +213,29 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume /** * Save current connections to the Database every 5 seconds. */ - Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) { - $payload = []; - foreach ($stats as $projectId => $value) { - $payload[$projectId] = $stats->get($projectId, 'connectionsTotal'); - } - if (empty($payload) || empty($statsDocument)) { - return; - } + // Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) { + // $payload = []; + // foreach ($stats as $projectId => $value) { + // $payload[$projectId] = $stats->get($projectId, 'connectionsTotal'); + // } + // if (empty($payload) || empty($statsDocument)) { + // return; + // } - try { - $database = getConsoleDB(); + // try { + // $database = getConsoleDB(); - $statsDocument - ->setAttribute('timestamp', DateTime::now()) - ->setAttribute('value', json_encode($payload)); + // $statsDocument + // ->setAttribute('timestamp', DateTime::now()) + // ->setAttribute('value', json_encode($payload)); - Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); - } catch (Throwable $th) { - call_user_func($logError, $th, "updateWorkerDocument"); - } finally { - $register->get('pools')->reclaim(); - } - }); + // Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); + // } catch (Throwable $th) { + // call_user_func($logError, $th, "updateWorkerDocument"); + // } finally { + // $register->get('pools')->reclaim(); + // } + // }); }); $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) { From 479ed3d0b12d9b3b3e0618af5ea2bf0940136f7c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 4 Apr 2024 18:51:40 +1300 Subject: [PATCH 41/61] Inline defaults --- app/init.php | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/app/init.php b/app/init.php index a54c993297..e59aeb081d 100644 --- a/app/init.php +++ b/app/init.php @@ -888,7 +888,7 @@ App::setResource('queue', function (Group $pools) { App::setResource('connectionForQueue', function () use ($connection) { return $connection; - }, []); + }); return $connection->getResource(); }, ['pools']); @@ -1134,16 +1134,16 @@ App::setResource('console', function () { App::setResource('connectionForProject', function () { return null; -}, []); +}); App::setResource('connectionForConsole', function () { return null; -}, []); +}); App::setResource('connectionForQueue', function () { return null; -}, []); +}); App::setResource('connectionsForCache', function () { return []; -}, []); +}); App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) { if ($project->isEmpty() || $project->getId() === 'console') { @@ -1152,12 +1152,11 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, $connection = $pools ->get($project->getAttribute('database')) - ->pop() - ; + ->pop(); App::setResource('connectionForProject', function () use ($connection) { return $connection; - }, []); + }); $dbAdapter = $connection->getResource(); @@ -1179,7 +1178,7 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) { App::setResource('connectionForConsole', function () use ($connection) { return $connection; - }, []); + }); $dbAdapter = $connection->getResource(); @@ -1195,7 +1194,7 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) { }, ['pools', 'cache']); App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { - $getProjectDB = function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { + return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForConsole; } @@ -1234,8 +1233,6 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, return $database; }; - - return $getProjectDB; }, ['pools', 'dbForConsole', 'cache']); App::setResource('cache', function (Group $pools) { From 2452d3a47920f77ef7d2f7c0b9cfea032a5c1276 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 4 Apr 2024 18:57:33 +1300 Subject: [PATCH 42/61] Reclaim only the used connection for realtime --- app/realtime.php | 282 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 203 insertions(+), 79 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 9c6ae850e4..84735b0f33 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -26,92 +26,151 @@ use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; use Utopia\Config\Config; use Utopia\Database\Database; +use Utopia\Pools\Group; +use Utopia\Registry\Registry; use Utopia\WebSocket\Server; use Utopia\WebSocket\Adapter; /** - * @var \Utopia\Registry\Registry $register + * @var Registry $register */ require_once __DIR__ . '/init.php'; -Runtime::enableCoroutine(SWOOLE_HOOK_ALL); +Runtime::enableCoroutine(); // Allows overriding -if (!function_exists("getConsoleDB")) { - function getConsoleDB(): Database +if (!function_exists('getConsoleDB')) { + /** + * @return array{Database, callable} + * @throws Exception|\Exception + */ + function getConsoleDB(): array { global $register; - /** @var \Utopia\Pools\Group $pools */ + /** @var Group $pools */ $pools = $register->get('pools'); - $dbAdapter = $pools + $dbConnection = $pools ->get('console') - ->pop() - ->getResource() - ; + ->pop(); - $database = new Database($dbAdapter, getCache()); + $dbAdapter = $dbConnection->getResource(); + + [$cache, $reclaimCache] = getCache(); + + $database = new Database($dbAdapter, $cache); $database ->setNamespace('_console') ->setMetadata('host', \gethostname()) ->setMetadata('project', '_console'); - return $database; + return [$database, function () use ($dbConnection, $reclaimCache) { + $dbConnection->reclaim(); + $reclaimCache(); + }]; } } // Allows overriding -if (!function_exists("getProjectDB")) { - function getProjectDB(Document $project): Database +if (!function_exists('getProjectDB')) { + /** + * @param Document $project + * @return array{Database, callable} + * @throws Exception + */ + function getProjectDB(Document $project): array { global $register; - /** @var \Utopia\Pools\Group $pools */ + /** @var Group $pools */ $pools = $register->get('pools'); if ($project->isEmpty() || $project->getId() === 'console') { return getConsoleDB(); } - $dbAdapter = $pools + $dbConnection = $pools ->get($project->getAttribute('database')) - ->pop() - ->getResource() - ; + ->pop(); - $database = new Database($dbAdapter, getCache()); + $dbAdapter = $dbConnection->getResource(); + + [$cache, $reclaimCache] = getCache(); + + $database = new Database($dbAdapter, $cache); $database ->setNamespace('_' . $project->getInternalId()) ->setMetadata('host', \gethostname()) ->setMetadata('project', $project->getId()); - return $database; + return [$database, function () use ($dbConnection, $reclaimCache) { + $dbConnection->reclaim(); + $reclaimCache(); + }]; } } // Allows overriding -if (!function_exists("getCache")) { - function getCache(): Cache +if (!function_exists('getCache')) { + /** + * @return array{Cache, callable} + * @throws Exception|\Exception + */ + function getCache(): array { global $register; - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + /** @var Group $pools */ + $pools = $register->get('pools'); $list = Config::getParam('pools-cache', []); + + $connections = []; $adapters = []; foreach ($list as $value) { - $adapters[] = $pools + $connection = $pools ->get($value) - ->pop() - ->getResource() - ; + ->pop(); + + $connections[] = $connection; + $adapters[] = $connection->getResource(); } - return new Cache(new Sharding($adapters)); + $cache = new Cache(new Sharding($adapters)); + + return [$cache, function () use ($connections) { + foreach ($connections as $connection) { + $connection->reclaim(); + } + }]; + } +} + +if (!function_exists('getPubSub')) { + /** + * @return array{Redis, callable} + * @throws Exception|\Exception + */ + function getPubSub(): array + { + global $register; + + /** @var Group $pools */ + $pools = $register->get('pools'); + + $connection = $pools + ->get('pubsub') + ->pop(); + + $redis = $connection->getResource(); + + return [$redis, function () use ($connection) { + $connection->reclaim(); + }]; } } @@ -133,13 +192,17 @@ $statsDocument = null; $workerNumber = swoole_cpu_num() * intval(App::getEnv('_APP_WORKER_PER_CORE', 6)); $adapter = new Adapter\Swoole(port: App::getEnv('PORT', 80)); + $adapter ->setPackageMaxLength(64000) // Default maximum Package Size (64kb) ->setWorkerNumber($workerNumber); $server = new Server($adapter); -$logError = function (Throwable $error, string $action) use ($register) { +function logError(Throwable $error, string $action): void +{ + global $register; + $logger = $register->get('logger'); if ($logger && !$error instanceof Exception) { @@ -173,11 +236,13 @@ $logError = function (Throwable $error, string $action) use ($register) { Console::error('[Error] Message: ' . $error->getMessage()); Console::error('[Error] File: ' . $error->getFile()); Console::error('[Error] Line: ' . $error->getLine()); -}; +} -$server->error($logError); +$server->error(function (Throwable $th, string $method) { + logError($th, $method); +}); -$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) { +$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) { sleep(5); // wait for the initial database schema to be ready Console::success('Server started successfully'); @@ -186,7 +251,12 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume */ go(function () use ($register, $containerId, &$statsDocument) { $attempts = 0; - $database = getConsoleDB(); + + /** + * @var Database $database + * @var callable $reclaim + */ + [$database, $reclaim] = getConsoleDB(); do { try { @@ -200,60 +270,74 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume 'value' => '{}' ]); - $statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document)); + $statsDocument = Authorization::skip(fn() => $database->createDocument('realtime', $document)); + break; } catch (Throwable) { Console::warning("Collection not ready. Retrying connection ({$attempts})..."); sleep(DATABASE_RECONNECT_SLEEP); } } while (true); - $register->get('pools')->reclaim(); + + $reclaim(); }); /** * Save current connections to the Database every 5 seconds. */ - // Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) { - // $payload = []; - // foreach ($stats as $projectId => $value) { - // $payload[$projectId] = $stats->get($projectId, 'connectionsTotal'); - // } - // if (empty($payload) || empty($statsDocument)) { - // return; - // } + Timer::tick(5000, function () use ($register, $stats, &$statsDocument) { + $payload = []; - // try { - // $database = getConsoleDB(); + foreach ($stats as $projectId => $value) { + $payload[$projectId] = $stats->get($projectId, 'connectionsTotal'); + } - // $statsDocument - // ->setAttribute('timestamp', DateTime::now()) - // ->setAttribute('value', json_encode($payload)); + if (empty($payload) || empty($statsDocument)) { + return; + } - // Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); - // } catch (Throwable $th) { - // call_user_func($logError, $th, "updateWorkerDocument"); - // } finally { - // $register->get('pools')->reclaim(); - // } - // }); + /** + * @var Database $database + * @var callable $reclaim + */ + [$database, $reclaim] = getConsoleDB(); + + $statsDocument + ->setAttribute('timestamp', DateTime::now()) + ->setAttribute('value', json_encode($payload)); + + try { + Authorization::skip(function () use ($database, $statsDocument) { + $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument); + }); + } catch (Throwable $th) { + logError($th, 'updateWorkerDocument'); + } finally { + $reclaim(); + } + }); }); -$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) { +$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) { Console::success('Worker ' . $workerId . ' started successfully'); $attempts = 0; $start = time(); - Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $logError) { + Timer::tick(5000, function () use ($server, $register, $realtime, $stats) { /** * Sending current connections to project channels on the console project every 5 seconds. */ if ($realtime->hasSubscriber('console', Role::users()->toString(), 'project')) { - $database = getConsoleDB(); + /** + * @var Database $database + * @var callable $reclaim + */ + [$database, $reclaim] = getConsoleDB(); $payload = []; - $list = Authorization::skip(fn () => $database->find('realtime', [ + $list = Authorization::skip(fn() => $database->find('realtime', [ Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)), ])); @@ -263,9 +347,9 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, foreach ($list as $document) { foreach (json_decode($document->getAttribute('value')) as $projectId => $value) { if (array_key_exists($projectId, $payload)) { - $payload[$projectId] += $value; + $payload[$projectId] += $value; } else { - $payload[$projectId] = $value; + $payload[$projectId] = $value; } } } @@ -294,8 +378,9 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, ])); } - $register->get('pools')->reclaim(); + $reclaim(); } + /** * Sending test message for SDK E2E tests every 5 seconds. */ @@ -327,9 +412,15 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, Attempting restart in 5 seconds (attempt #' . $attempts . ')'); sleep(5); // 5 sec delay between connection attempts } + $start = time(); - $redis = $register->get('pools')->get('pubsub')->pop()->getResource(); /** @var Redis $redis */ + /** + * @var Redis $redis + * @var callable $reclaimForRedis + */ + [$redis, $reclaimForRedis] = getPubSub(); + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); if ($redis->ping(true)) { @@ -348,17 +439,34 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) { $connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId])); - $consoleDatabase = getConsoleDB(); - $project = Authorization::skip(fn() => $consoleDatabase->getDocument('projects', $projectId)); - $database = getProjectDB($project); - $user = $database->getDocument('users', $userId); + /** + * @var Database $dbForConsole + * @var Database $dbForProject + * @var callable $reclaimForConsole + * @var callable $reclaimForProject + */ + [$dbForConsole, $reclaimForConsole] = getConsoleDB(); + + $project = Authorization::skip(fn() => $dbForConsole->getDocument('projects', $projectId)); + + [$dbForProject, $reclaimForProject] = getProjectDB($project); + + $user = $dbForProject->getDocument('users', $userId); $roles = Auth::getRoles($user); $realtime->subscribe($projectId, $connection, $roles, $realtime->connections[$connection]['channels']); - $register->get('pools')->reclaim(); + /** + * If we successfully reclaim, clear the callbacks + * so the finally block doesn't try to reclaim again. + */ + $reclaimForConsole(); + $reclaimForConsole = null; + + $reclaimForProject(); + $reclaimForProject = null; } } @@ -383,21 +491,28 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, } }); } catch (Throwable $th) { - call_user_func($logError, $th, "pubSubConnection"); - + logError($th, 'pubSubConnection'); Console::error('Pub/sub error: ' . $th->getMessage()); $attempts++; sleep(DATABASE_RECONNECT_SLEEP); continue; } finally { - $register->get('pools')->reclaim(); + if (isset($reclaimForRedis)) { + $reclaimForRedis(); + } + if (isset($reclaimForConsole)) { + $reclaimForConsole(); + } + if (isset($reclaimForProject)) { + $reclaimForProject(); + } } } Console::error('Failed to restart pub/sub...'); }); -$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $logError) { +$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) { $app = new App('UTC'); $request = new Request($request); $response = new Response(new SwooleResponse()); @@ -419,9 +534,13 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID'); } - $dbForProject = getProjectDB($project); - $console = $app->getResource('console'); /** @var Document $console */ - $user = $app->getResource('user'); /** @var Document $user */ + [$dbForProject, $reclaimForProject] = getProjectDB($project); + + /** @var Document $console */ + $console = $app->getResource('console'); + + /** @var Document $user */ + $user = $app->getResource('user'); /* * Abuse Check @@ -481,7 +600,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $stats->incr($project->getId(), 'connections'); $stats->incr($project->getId(), 'connectionsTotal'); } catch (Throwable $th) { - call_user_func($logError, $th, "initServer"); + logError($th, 'initServer'); $response = [ 'type' => 'error', @@ -500,7 +619,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, Console::error('[Error] Message: ' . $response['data']['message']); } } finally { - $register->get('pools')->reclaim(); + if (isset($reclaimForProject)) { + $reclaimForProject(); + } } }); @@ -508,7 +629,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re try { $response = new Response(new SwooleResponse()); $projectId = $realtime->connections[$connection]['projectId']; - $database = getConsoleDB(); + [$database, $reclaim] = getConsoleDB(); if ($projectId !== 'console') { $project = Authorization::skip(fn() => $database->getDocument('projects', $projectId)); @@ -598,7 +719,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re $server->close($connection, $th->getCode()); } } finally { - $register->get('pools')->reclaim(); + if (isset($reclaim)) { + $reclaim(); + } } }); @@ -606,6 +729,7 @@ $server->onClose(function (int $connection) use ($realtime, $stats) { if (array_key_exists($connection, $realtime->connections)) { $stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal'); } + $realtime->unsubscribe($connection); Console::info('Connection close: ' . $connection); From ccf437dac47af328420afd3bdb12e7e18ad17792 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 4 Apr 2024 19:56:09 +1300 Subject: [PATCH 43/61] Update db for relationships and object as array attributes fixes --- composer.json | 2 +- composer.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index ca7a350ebc..186ba1eff3 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cache": "0.9.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.45.9", + "utopia-php/database": "0.45.11", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.1.*", "utopia-php/framework": "0.33.*", diff --git a/composer.lock b/composer.lock index 742c6ff80c..c8040a54de 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "fca40e29f72e83282e4c492c3a81674c", + "content-hash": "d4bead528175f70ccaf4f4b8ec03a486", "packages": [ { "name": "adhocore/jwt", @@ -1191,16 +1191,16 @@ }, { "name": "utopia-php/database", - "version": "0.45.9", + "version": "0.45.11", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "e43e4b42f2a43e56dcfdc99ca527dfc885ca0667" + "reference": "262bebfdfb25a37961c218afac0e647f7f23dbac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/e43e4b42f2a43e56dcfdc99ca527dfc885ca0667", - "reference": "e43e4b42f2a43e56dcfdc99ca527dfc885ca0667", + "url": "https://api.github.com/repos/utopia-php/database/zipball/262bebfdfb25a37961c218afac0e647f7f23dbac", + "reference": "262bebfdfb25a37961c218afac0e647f7f23dbac", "shasum": "" }, "require": { @@ -1241,9 +1241,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.45.9" + "source": "https://github.com/utopia-php/database/tree/0.45.11" }, - "time": "2024-04-02T09:22:05+00:00" + "time": "2024-04-04T02:23:02+00:00" }, { "name": "utopia-php/domains", @@ -3220,16 +3220,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.27.0", + "version": "1.28.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757" + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/86e4d5a4b036f8f0be1464522f4c6b584c452757", - "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", "shasum": "" }, "require": { @@ -3261,9 +3261,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.27.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.28.0" }, - "time": "2024-03-21T13:14:53+00:00" + "time": "2024-04-03T18:51:33+00:00" }, { "name": "phpunit/php-code-coverage", @@ -5169,5 +5169,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } From 6d5b08760536deb18357791e23b50bb1be30da00 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Fri, 5 Apr 2024 22:39:52 +0530 Subject: [PATCH 44/61] Bump executor version to 0.5.1 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index a4daa80e8c..4b0f88afb6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -751,7 +751,7 @@ services: hostname: appwrite-executor <<: *x-logging stop_signal: SIGINT - image: openruntimes/executor:0.5.0 + image: openruntimes/executor:0.5.1 restart: unless-stopped networks: - appwrite From 293d85525c625c8d51eb7fbb0df28cb01e07fe25 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 8 Apr 2024 13:43:53 +1200 Subject: [PATCH 45/61] Fix on message db resource fetch --- app/realtime.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 84735b0f33..abe27bdde6 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -629,11 +629,11 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re try { $response = new Response(new SwooleResponse()); $projectId = $realtime->connections[$connection]['projectId']; - [$database, $reclaim] = getConsoleDB(); + [$database, $reclaimForConsole] = getConsoleDB(); if ($projectId !== 'console') { $project = Authorization::skip(fn() => $database->getDocument('projects', $projectId)); - $database = getProjectDB($project); + [$database, $reclaimForProject] = getProjectDB($project); } else { $project = null; } @@ -719,8 +719,11 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re $server->close($connection, $th->getCode()); } } finally { - if (isset($reclaim)) { - $reclaim(); + if (isset($reclaimForConsole)) { + $reclaimForConsole(); + } + if (isset($reclaimForProject)) { + $reclaimForProject(); } } }); From fc4cbb81740dafe12920c5a6399e550def50156a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 8 Apr 2024 14:40:51 +1200 Subject: [PATCH 46/61] Move getConsole tick inside try/catch --- app/realtime.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index abe27bdde6..8fdfce3e4a 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -296,24 +296,26 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume return; } - /** - * @var Database $database - * @var callable $reclaim - */ - [$database, $reclaim] = getConsoleDB(); - - $statsDocument - ->setAttribute('timestamp', DateTime::now()) - ->setAttribute('value', json_encode($payload)); - try { + /** + * @var Database $database + * @var callable $reclaim + */ + [$database, $reclaim] = getConsoleDB(); + + $statsDocument + ->setAttribute('timestamp', DateTime::now()) + ->setAttribute('value', json_encode($payload)); + Authorization::skip(function () use ($database, $statsDocument) { $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument); }); } catch (Throwable $th) { logError($th, 'updateWorkerDocument'); } finally { - $reclaim(); + if (isset($reclaim)) { + $reclaim(); + } } }); }); From 899bba64d1b97178cd241d286765413af5238fc5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 8 Apr 2024 14:55:41 +1200 Subject: [PATCH 47/61] Add try/catch/reclaim around sending stats --- app/realtime.php | 130 ++++++++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 53 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 8fdfce3e4a..8a0b2f2c38 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -252,15 +252,16 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume go(function () use ($register, $containerId, &$statsDocument) { $attempts = 0; - /** - * @var Database $database - * @var callable $reclaim - */ - [$database, $reclaim] = getConsoleDB(); - do { try { + /** + * @var Database $database + * @var callable $reclaim + */ + [$database, $reclaim] = getConsoleDB(); + $attempts++; + $document = new Document([ '$id' => ID::unique(), '$collection' => ID::custom('realtime'), @@ -270,7 +271,9 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume 'value' => '{}' ]); - $statsDocument = Authorization::skip(fn() => $database->createDocument('realtime', $document)); + $statsDocument = Authorization::skip(function () use ($database, $document) { + return $database->createDocument('realtime', $document); + }); break; } catch (Throwable) { @@ -279,7 +282,9 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume } } while (true); - $reclaim(); + if (isset($reclaim)) { + $reclaim(); + } }); /** @@ -331,56 +336,64 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, * Sending current connections to project channels on the console project every 5 seconds. */ if ($realtime->hasSubscriber('console', Role::users()->toString(), 'project')) { - /** - * @var Database $database - * @var callable $reclaim - */ - [$database, $reclaim] = getConsoleDB(); + try { + /** + * @var Database $database + * @var callable $reclaim + */ + [$database, $reclaim] = getConsoleDB(); - $payload = []; + $payload = []; - $list = Authorization::skip(fn() => $database->find('realtime', [ - Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)), - ])); + $list = Authorization::skip(function () use ($database) { + return $database->find('realtime', [ + Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)), + ]); + }); - /** - * Aggregate stats across containers. - */ - foreach ($list as $document) { - foreach (json_decode($document->getAttribute('value')) as $projectId => $value) { - if (array_key_exists($projectId, $payload)) { - $payload[$projectId] += $value; - } else { - $payload[$projectId] = $value; + /** + * Aggregate stats across containers. + */ + foreach ($list as $document) { + foreach (json_decode($document->getAttribute('value')) as $projectId => $value) { + if (array_key_exists($projectId, $payload)) { + $payload[$projectId] += $value; + } else { + $payload[$projectId] = $value; + } } } - } - foreach ($stats as $projectId => $value) { - if (!array_key_exists($projectId, $payload)) { - continue; - } + foreach ($stats as $projectId => $value) { + if (!array_key_exists($projectId, $payload)) { + continue; + } - $event = [ - 'project' => 'console', - 'roles' => ['team:' . $stats->get($projectId, 'teamId')], - 'data' => [ - 'events' => ['stats.connections'], - 'channels' => ['project'], - 'timestamp' => DateTime::formatTz(DateTime::now()), - 'payload' => [ - $projectId => $payload[$projectId] + $event = [ + 'project' => 'console', + 'roles' => ['team:' . $stats->get($projectId, 'teamId')], + 'data' => [ + 'events' => ['stats.connections'], + 'channels' => ['project'], + 'timestamp' => DateTime::formatTz(DateTime::now()), + 'payload' => [ + $projectId => $payload[$projectId] + ] ] - ] - ]; + ]; - $server->send($realtime->getSubscribers($event), json_encode([ - 'type' => 'event', - 'data' => $event['data'] - ])); + $server->send($realtime->getSubscribers($event), json_encode([ + 'type' => 'event', + 'data' => $event['data'] + ])); + } + } catch (Throwable $th) { + logError($th, 'sendStats'); + } finally { + if (isset($reclaim)) { + $reclaim(); + } } - - $reclaim(); } /** @@ -450,7 +463,9 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, */ [$dbForConsole, $reclaimForConsole] = getConsoleDB(); - $project = Authorization::skip(fn() => $dbForConsole->getDocument('projects', $projectId)); + $project = Authorization::skip(function () use ($dbForConsole, $projectId) { + return $dbForConsole->getDocument('projects', $projectId); + }); [$dbForProject, $reclaimForProject] = getProjectDB($project); @@ -521,9 +536,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, Console::info("Connection open (user: {$connection})"); - App::setResource('pools', fn() => $register->get('pools')); - App::setResource('request', fn() => $request); - App::setResource('response', fn() => $response); + App::setResource('pools', function () use ($register) { + return $register->get('pools'); + }); + App::setResource('request', function () use ($request) { + return $request; + }); + App::setResource('response', function () use ($response) { + return $response; + }); try { /** @var Document $project */ @@ -634,7 +655,10 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re [$database, $reclaimForConsole] = getConsoleDB(); if ($projectId !== 'console') { - $project = Authorization::skip(fn() => $database->getDocument('projects', $projectId)); + $project = Authorization::skip(function () use ($database, $projectId) { + return $database->getDocument('projects', $projectId); + }); + [$database, $reclaimForProject] = getProjectDB($project); } else { $project = null; From 7b66cc56728f7c1a3a72f106f46dbdef100e0b0b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 8 Apr 2024 15:48:57 +1200 Subject: [PATCH 48/61] Revert getProjectDB change --- app/init.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/init.php b/app/init.php index e59aeb081d..5844cafa5c 100644 --- a/app/init.php +++ b/app/init.php @@ -1194,7 +1194,9 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) { }, ['pools', 'cache']); App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { - return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { + $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools + + $getProjectDB = function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForConsole; } @@ -1233,6 +1235,8 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, return $database; }; + + return $getProjectDB; }, ['pools', 'dbForConsole', 'cache']); App::setResource('cache', function (Group $pools) { From a0051eaa819001e8af789b25b154a746e89335e4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 8 Apr 2024 19:59:36 +1200 Subject: [PATCH 49/61] Update database --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 186ba1eff3..0e8575e7c1 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cache": "0.9.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.45.11", + "utopia-php/database": "0.45.12", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.1.*", "utopia-php/framework": "0.33.*", diff --git a/composer.lock b/composer.lock index c8040a54de..c8ce177133 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "d4bead528175f70ccaf4f4b8ec03a486", + "content-hash": "e90287b00089ad8e8059e6e157f3a6c8", "packages": [ { "name": "adhocore/jwt", @@ -1191,16 +1191,16 @@ }, { "name": "utopia-php/database", - "version": "0.45.11", + "version": "0.45.12", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "262bebfdfb25a37961c218afac0e647f7f23dbac" + "reference": "805efb95b19d555b6cfd2244e36bfe08289f24f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/262bebfdfb25a37961c218afac0e647f7f23dbac", - "reference": "262bebfdfb25a37961c218afac0e647f7f23dbac", + "url": "https://api.github.com/repos/utopia-php/database/zipball/805efb95b19d555b6cfd2244e36bfe08289f24f5", + "reference": "805efb95b19d555b6cfd2244e36bfe08289f24f5", "shasum": "" }, "require": { @@ -1241,9 +1241,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.45.11" + "source": "https://github.com/utopia-php/database/tree/0.45.12" }, - "time": "2024-04-04T02:23:02+00:00" + "time": "2024-04-08T07:50:55+00:00" }, { "name": "utopia-php/domains", From 730e7319df25ffb0c1314086eca9df912e4730d6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 9 Apr 2024 17:03:28 +1200 Subject: [PATCH 50/61] Use connection group to reclaim all use connections for request --- app/http.php | 27 +++------- app/init.php | 65 +++++++---------------- src/Appwrite/Utopia/Queue/Connections.php | 56 +++++++++++++++++++ 3 files changed, 83 insertions(+), 65 deletions(-) create mode 100644 src/Appwrite/Utopia/Queue/Connections.php diff --git a/app/http.php b/app/http.php index 05e989b330..91da4e8cab 100644 --- a/app/http.php +++ b/app/http.php @@ -2,6 +2,7 @@ require_once __DIR__ . '/../vendor/autoload.php'; +use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Response; use Swoole\Process; use Swoole\Http\Server; @@ -323,27 +324,13 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $swooleResponse->end(\json_encode($output)); } finally { - $connectionForConsole = $app->getResource('connectionForConsole'); - $connectionForProject = $app->getResource('connectionForProject'); - $connectionForQueue = $app->getResource('connectionForQueue'); - $connectionsForCache = $app->getResource('connectionsForCache'); + /** + * @var Connections $connections + */ + $connections = $app->getResource('connections'); - if (!is_null($connectionForConsole)) { - $connectionForConsole->reclaim(); - } - - if (!is_null($connectionForProject)) { - $connectionForProject->reclaim(); - } - - if (!is_null($connectionForQueue)) { - $connectionForQueue->reclaim(); - } - - if (!empty($connectionsForCache)) { - foreach ($connectionsForCache as $connection) { - $connection->reclaim(); - } + if (!empty($connections)) { + $connections->reclaim(); } } }); diff --git a/app/init.php b/app/init.php index 5844cafa5c..1830fd1763 100644 --- a/app/init.php +++ b/app/init.php @@ -33,6 +33,7 @@ use Appwrite\Network\Validator\Email; use Appwrite\Network\Validator\Origin; use Appwrite\OpenSSL\OpenSSL; use Appwrite\URL\URL as AppwriteURL; +use Appwrite\Utopia\Queue\Connections; use Utopia\App; use Utopia\Logger\Logger; use Utopia\Cache\Adapter\Redis as RedisCache; @@ -881,17 +882,14 @@ App::setResource('locale', fn() => new Locale(App::getEnv('_APP_LOCALE', 'en'))) App::setResource('localeCodes', function () { return array_map(fn($locale) => $locale['code'], Config::getParam('locale-codes', [])); }); - // Queues -App::setResource('queue', function (Group $pools) { +App::setResource('queue', function (Group $pools, Connections $connections) { $connection = $pools->get('queue')->pop(); - App::setResource('connectionForQueue', function () use ($connection) { - return $connection; - }); + $connections->add($connection); return $connection->getResource(); -}, ['pools']); +}, ['pools', 'connections']); App::setResource('queueForMessaging', function (Connection $queue) { return new Phone($queue); }, ['queue']); @@ -1132,20 +1130,11 @@ App::setResource('console', function () { ]); }, []); -App::setResource('connectionForProject', function () { - return null; -}); -App::setResource('connectionForConsole', function () { - return null; -}); -App::setResource('connectionForQueue', function () { - return null; -}); -App::setResource('connectionsForCache', function () { - return []; +App::setResource('connections', function () { + return new Connections(); }); -App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) { +App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project, Connections $connections) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForConsole; } @@ -1154,9 +1143,7 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, ->get($project->getAttribute('database')) ->pop(); - App::setResource('connectionForProject', function () use ($connection) { - return $connection; - }); + $connections->add($connection); $dbAdapter = $connection->getResource(); @@ -1169,16 +1156,14 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); return $database; -}, ['pools', 'dbForConsole', 'cache', 'project']); +}, ['pools', 'dbForConsole', 'cache', 'project', 'connections']); -App::setResource('dbForConsole', function (Group $pools, Cache $cache) { +App::setResource('dbForConsole', function (Group $pools, Cache $cache, Connections $connections) { $connection = $pools ->get('console') ->pop(); - App::setResource('connectionForConsole', function () use ($connection) { - return $connection; - }); + $connections->add($connection); $dbAdapter = $connection->getResource(); @@ -1191,12 +1176,12 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) { ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); return $database; -}, ['pools', 'cache']); +}, ['pools', 'cache', 'connections']); -App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { +App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, Cache $cache, Connections $connections) { $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - $getProjectDB = function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { + $getProjectDB = function (Document $project) use ($pools, $dbForConsole, $cache, $connections, &$databases) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForConsole; } @@ -1208,8 +1193,6 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $database ->setNamespace('_' . $project->getInternalId()) - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); return $database; @@ -1219,44 +1202,36 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, ->get($databaseName) ->pop(); - $dbAdapter = $connection->getResource(); + $connections->add($connection); - App::setResource('connectionForProject', function () use ($connection) { - return $connection; - }, []); + $dbAdapter = $connection->getResource(); $database = new Database($dbAdapter, $cache); $database ->setNamespace('_' . $project->getInternalId()) - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); return $database; }; return $getProjectDB; -}, ['pools', 'dbForConsole', 'cache']); +}, ['pools', 'dbForConsole', 'cache', 'connections']); -App::setResource('cache', function (Group $pools) { +App::setResource('cache', function (Group $pools, Connections $connections) { $list = Config::getParam('pools-cache', []); $adapters = []; - $connections = []; foreach ($list as $value) { $connection = $pools ->get($value) ->pop(); - $connections[] = $connection; + $connections->add($connection); + $adapters[] = $connection->getResource(); } - App::setResource('connectionsForCache', function () use ($connections) { - return $connections; - }, []); - return new Cache(new Sharding($adapters)); }, ['pools']); diff --git a/src/Appwrite/Utopia/Queue/Connections.php b/src/Appwrite/Utopia/Queue/Connections.php new file mode 100644 index 0000000000..e3d5740b97 --- /dev/null +++ b/src/Appwrite/Utopia/Queue/Connections.php @@ -0,0 +1,56 @@ + + */ + protected array $connections = []; + + /** + * @param Connection $connection + * @return self + */ + public function add(Connection $connection): self + { + $this->connections[$connection->getID()] = $connection; + return $this; + } + + /** + * @param string $id + * @return Connection + * @throws \Exception + */ + public function get(string $id): Connection + { + return $this->connections[$id] ?? throw new \Exception("Connection '{$id}' not found"); + } + + /** + * @param string $id + * @return self + */ + public function remove(string $id): self + { + unset($this->connections[$id]); + return $this; + } + + /** + * @return self + * @throws \Exception + */ + public function reclaim(): self + { + foreach ($this->connections as $connection) { + $connection->reclaim(); + } + + return $this; + } +} From fefb60557df8a220bf1efc104e3adc867d3471b7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 9 Apr 2024 17:04:31 +1200 Subject: [PATCH 51/61] Reclaim only used connections for workers --- app/worker.php | 99 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 30 deletions(-) diff --git a/app/worker.php b/app/worker.php index 32ce35e033..2982f7af1f 100644 --- a/app/worker.php +++ b/app/worker.php @@ -16,6 +16,7 @@ use Appwrite\Event\Phone; use Appwrite\Event\Usage; use Appwrite\Event\UsageDump; use Appwrite\Platform\Appwrite; +use Appwrite\Utopia\Queue\Connections; use Swoole\Runtime; use Utopia\App; use Utopia\Cache\Adapter\Sharding; @@ -36,25 +37,32 @@ use Utopia\Pools\Group; use Utopia\Queue\Connection; Authorization::disable(); -Runtime::enableCoroutine(SWOOLE_HOOK_ALL); +Runtime::enableCoroutine(); Server::setResource('register', fn () => $register); -Server::setResource('dbForConsole', function (Cache $cache, Registry $register) { +Server::setResource('connections', function () { + return new Connections(); +}); + +Server::setResource('dbForConsole', function (Cache $cache, Registry $register, Connections $connections) { $pools = $register->get('pools'); - $database = $pools + + $connection = $pools ->get('console') - ->pop() - ->getResource() - ; + ->pop(); + + $connections->add($connection); + + $database = $connection->getResource(); $adapter = new Database($database, $cache); $adapter->setNamespace('_console'); return $adapter; -}, ['cache', 'register']); +}, ['cache', 'register', 'connections']); -Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Database $dbForConsole) { +Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Database $dbForConsole, Connections $connections) { $payload = $message->getPayload() ?? []; $project = new Document($payload['project'] ?? []); @@ -63,16 +71,19 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register, } $pools = $register->get('pools'); - $database = $pools + + $connection = $pools ->get($project->getAttribute('database')) - ->pop() - ->getResource() - ; + ->pop(); + + $database = $connection->getResource(); + + $connections->add($connection); $adapter = new Database($database, $cache); $adapter->setNamespace('_' . $project->getInternalId()); return $adapter; -}, ['cache', 'register', 'message', 'dbForConsole']); +}, ['cache', 'register', 'message', 'dbForConsole', 'connections']); Server::setResource('project', function (Message $message, Database $dbForConsole) { $payload = $message->getPayload() ?? []; @@ -81,14 +92,14 @@ Server::setResource('project', function (Message $message, Database $dbForConsol if ($project->getId() === 'console') { return $project; } + return $dbForConsole->getDocument('projects', $project->getId()); - ; }, ['message', 'dbForConsole']); -Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { +Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, Cache $cache, Connections $connections) { $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases): Database { + return function (Document $project) use ($pools, $dbForConsole, $cache, $connections, &$databases): Database { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForConsole; } @@ -101,10 +112,13 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso return $database; } - $dbAdapter = $pools + $dbConnection = $pools ->get($databaseName) - ->pop() - ->getResource(); + ->pop(); + + $dbAdapter = $dbConnection->getResource(); + + $connections->add($dbConnection); $database = new Database($dbAdapter, $cache); @@ -114,7 +128,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso return $database; }; -}, ['pools', 'dbForConsole', 'cache']); +}, ['pools', 'dbForConsole', 'cache', 'connections']); Server::setResource('abuseRetention', function () { return DateTime::addSeconds(new \DateTime(), -1 * App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400)); @@ -128,82 +142,104 @@ Server::setResource('executionRetention', function () { return DateTime::addSeconds(new \DateTime(), -1 * App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); }); -Server::setResource('cache', function (Registry $register) { +Server::setResource('cache', function (Registry $register, Connections $connections) { $pools = $register->get('pools'); $list = Config::getParam('pools-cache', []); $adapters = []; foreach ($list as $value) { - $adapters[] = $pools + $connection = $pools ->get($value) - ->pop() - ->getResource() - ; + ->pop(); + + $connections->add($connection); + + $adapters[] = $connection->getResource(); } return new Cache(new Sharding($adapters)); -}, ['register']); +}, ['register', 'connections']); + Server::setResource('log', fn() => new Log()); + Server::setResource('queueForUsage', function (Connection $queue) { return new Usage($queue); }, ['queue']); + Server::setResource('queueForUsageDump', function (Connection $queue) { return new UsageDump($queue); }, ['queue']); + Server::setResource('queue', function (Group $pools) { return $pools->get('queue')->pop()->getResource(); }, ['pools']); + Server::setResource('queueForDatabase', function (Connection $queue) { return new EventDatabase($queue); }, ['queue']); + Server::setResource('queueForMessaging', function (Connection $queue) { return new Phone($queue); }, ['queue']); + Server::setResource('queueForMails', function (Connection $queue) { return new Mail($queue); }, ['queue']); + Server::setResource('queueForBuilds', function (Connection $queue) { return new Build($queue); }, ['queue']); + Server::setResource('queueForDeletes', function (Connection $queue) { return new Delete($queue); }, ['queue']); + Server::setResource('queueForEvents', function (Connection $queue) { return new Event($queue); }, ['queue']); + Server::setResource('queueForAudits', function (Connection $queue) { return new Audit($queue); }, ['queue']); + Server::setResource('queueForFunctions', function (Connection $queue) { return new Func($queue); }, ['queue']); + Server::setResource('queueForCertificates', function (Connection $queue) { return new Certificate($queue); }, ['queue']); + Server::setResource('queueForMigrations', function (Connection $queue) { return new Migration($queue); }, ['queue']); + Server::setResource('logger', function (Registry $register) { return $register->get('logger'); }, ['register']); + Server::setResource('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); + Server::setResource('getFunctionsDevice', function () { return function (string $projectId) { return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $projectId); }; }); + Server::setResource('getFilesDevice', function () { return function (string $projectId) { return getDevice(APP_STORAGE_UPLOADS . '/app-' . $projectId); }; }); + Server::setResource('getBuildsDevice', function () { return function (string $projectId) { return getDevice(APP_STORAGE_BUILDS . '/app-' . $projectId); }; }); + Server::setResource('getCacheDevice', function () { return function (string $projectId) { return getDevice(APP_STORAGE_CACHE . '/app-' . $projectId); @@ -254,9 +290,9 @@ $worker = $platform->getWorker(); $worker ->shutdown() - ->inject('pools') - ->action(function (Group $pools) { - $pools->reclaim(); + ->inject('connections') + ->action(function (Connections $connections) { + $connections->reclaim(); }); $worker @@ -265,7 +301,10 @@ $worker ->inject('logger') ->inject('log') ->inject('project') - ->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project) { + ->inject('connections') + ->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Connections $connections) { + $connections->reclaim(); + $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); if ($error instanceof PDOException) { From 4bef6125f700cbc4158f51dae663565c729aca15 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 9 Apr 2024 17:05:44 +1200 Subject: [PATCH 52/61] Use on worker stop to properly reclaim realtime redis connections and unsubscribe --- app/realtime.php | 35 +++++++++++++++++++++-------------- composer.json | 2 +- composer.lock | 30 ++++++++++++------------------ 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 8a0b2f2c38..ce61f0f2e9 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -38,6 +38,8 @@ require_once __DIR__ . '/init.php'; Runtime::enableCoroutine(); +$redisConnections = []; + // Allows overriding if (!function_exists('getConsoleDB')) { /** @@ -60,11 +62,7 @@ if (!function_exists('getConsoleDB')) { [$cache, $reclaimCache] = getCache(); $database = new Database($dbAdapter, $cache); - - $database - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', '_console'); + $database->setNamespace('_console'); return [$database, function () use ($dbConnection, $reclaimCache) { $dbConnection->reclaim(); @@ -100,11 +98,7 @@ if (!function_exists('getProjectDB')) { [$cache, $reclaimCache] = getCache(); $database = new Database($dbAdapter, $cache); - - $database - ->setNamespace('_' . $project->getInternalId()) - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()); + $database->setNamespace('_' . $project->getInternalId()); return [$database, function () use ($dbConnection, $reclaimCache) { $dbConnection->reclaim(); @@ -325,7 +319,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume }); }); -$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) { +$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, &$redisConnections) { Console::success('Worker ' . $workerId . ' started successfully'); $attempts = 0; @@ -436,6 +430,8 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, */ [$redis, $reclaimForRedis] = getPubSub(); + $redisConnections[$workerId] = [$redis, $reclaimForRedis]; + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); if ($redis->ping(true)) { @@ -514,9 +510,6 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, sleep(DATABASE_RECONNECT_SLEEP); continue; } finally { - if (isset($reclaimForRedis)) { - $reclaimForRedis(); - } if (isset($reclaimForConsole)) { $reclaimForConsole(); } @@ -529,6 +522,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, Console::error('Failed to restart pub/sub...'); }); +$server->onWorkerStop(function (int $workerId) use ($redisConnections) { + /** + * @var Redis $redis + * @var callable $reclaim + */ + [$redis, $reclaim] = $redisConnections[$workerId] ?? null; + + $redis?->unsubscribe(['realtime']); + + if ($reclaim) { + $reclaim(); + } +}); + $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) { $app = new App('UTC'); $request = new Request($request); diff --git a/composer.json b/composer.json index 0e8575e7c1..42eedfaba6 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/storage": "0.18.*", "utopia-php/swoole": "0.5.*", "utopia-php/vcs": "0.6.*", - "utopia-php/websocket": "0.1.*", + "utopia-php/websocket": "dev-feat-worker-stop", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", "phpmailer/phpmailer": "6.8.0", diff --git a/composer.lock b/composer.lock index c8ce177133..70fe96499c 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "e90287b00089ad8e8059e6e157f3a6c8", + "content-hash": "ce7a7f429d68836b8667e2b02440fc3f", "packages": [ { "name": "adhocore/jwt", @@ -2273,22 +2273,24 @@ }, { "name": "utopia-php/websocket", - "version": "0.1.0", + "version": "dev-feat-worker-stop", "source": { "type": "git", "url": "https://github.com/utopia-php/websocket.git", - "reference": "51fcb86171400d8aa40d76c54593481fd273dab5" + "reference": "b7adfab69d4c48a60272fa4cb3328f9b400b0ffa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/websocket/zipball/51fcb86171400d8aa40d76c54593481fd273dab5", - "reference": "51fcb86171400d8aa40d76c54593481fd273dab5", + "url": "https://api.github.com/repos/utopia-php/websocket/zipball/b7adfab69d4c48a60272fa4cb3328f9b400b0ffa", + "reference": "b7adfab69d4c48a60272fa4cb3328f9b400b0ffa", "shasum": "" }, "require": { "php": ">=8.0" }, "require-dev": { + "laravel/pint": "^1.15", + "phpstan/phpstan": "^1.8", "phpunit/phpunit": "^9.5.5", "swoole/ide-helper": "4.6.6", "textalk/websocket": "1.5.2", @@ -2305,16 +2307,6 @@ "license": [ "MIT" ], - "authors": [ - { - "name": "Eldad Fux", - "email": "eldad@appwrite.io" - }, - { - "name": "Torsten Dittmann", - "email": "torsten@appwrite.io" - } - ], "description": "A simple abstraction for WebSocket servers.", "keywords": [ "framework", @@ -2325,9 +2317,9 @@ ], "support": { "issues": "https://github.com/utopia-php/websocket/issues", - "source": "https://github.com/utopia-php/websocket/tree/0.1.0" + "source": "https://github.com/utopia-php/websocket/tree/feat-worker-stop" }, - "time": "2021-12-20T10:50:09+00:00" + "time": "2024-04-09T04:48:45+00:00" }, { "name": "webmozart/assert", @@ -5145,7 +5137,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "utopia-php/websocket": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { From 388f450d895d919e8964043562fb6bae249d6b27 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 9 Apr 2024 18:11:08 +1200 Subject: [PATCH 53/61] Fix missing injection --- app/init.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/init.php b/app/init.php index 1830fd1763..86f5ad5b1e 100644 --- a/app/init.php +++ b/app/init.php @@ -1233,7 +1233,7 @@ App::setResource('cache', function (Group $pools, Connections $connections) { } return new Cache(new Sharding($adapters)); -}, ['pools']); +}, ['pools', 'connections']); App::setResource('deviceLocal', function () { return new Local(); From b50ec49ac13b0b13f43dab8a6cb01cd3dddea233 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 9 Apr 2024 20:26:42 +1200 Subject: [PATCH 54/61] Move `Connections` to pools namespace --- app/http.php | 2 +- app/init.php | 2 +- app/worker.php | 2 +- .../Utopia/{Queue => Pools}/Connections.php | 20 ++++++++----------- 4 files changed, 11 insertions(+), 15 deletions(-) rename src/Appwrite/Utopia/{Queue => Pools}/Connections.php (69%) diff --git a/app/http.php b/app/http.php index 91da4e8cab..008a355550 100644 --- a/app/http.php +++ b/app/http.php @@ -2,7 +2,7 @@ require_once __DIR__ . '/../vendor/autoload.php'; -use Appwrite\Utopia\Queue\Connections; +use Appwrite\Utopia\Pools\Connections; use Appwrite\Utopia\Response; use Swoole\Process; use Swoole\Http\Server; diff --git a/app/init.php b/app/init.php index 86f5ad5b1e..829911f904 100644 --- a/app/init.php +++ b/app/init.php @@ -33,7 +33,7 @@ use Appwrite\Network\Validator\Email; use Appwrite\Network\Validator\Origin; use Appwrite\OpenSSL\OpenSSL; use Appwrite\URL\URL as AppwriteURL; -use Appwrite\Utopia\Queue\Connections; +use Appwrite\Utopia\Pools\Connections; use Utopia\App; use Utopia\Logger\Logger; use Utopia\Cache\Adapter\Redis as RedisCache; diff --git a/app/worker.php b/app/worker.php index 2982f7af1f..99f4154c98 100644 --- a/app/worker.php +++ b/app/worker.php @@ -16,7 +16,7 @@ use Appwrite\Event\Phone; use Appwrite\Event\Usage; use Appwrite\Event\UsageDump; use Appwrite\Platform\Appwrite; -use Appwrite\Utopia\Queue\Connections; +use Appwrite\Utopia\Pools\Connections; use Swoole\Runtime; use Utopia\App; use Utopia\Cache\Adapter\Sharding; diff --git a/src/Appwrite/Utopia/Queue/Connections.php b/src/Appwrite/Utopia/Pools/Connections.php similarity index 69% rename from src/Appwrite/Utopia/Queue/Connections.php rename to src/Appwrite/Utopia/Pools/Connections.php index e3d5740b97..bf9a44bdb8 100644 --- a/src/Appwrite/Utopia/Queue/Connections.php +++ b/src/Appwrite/Utopia/Pools/Connections.php @@ -1,6 +1,6 @@ connections[$id] ?? throw new \Exception("Connection '{$id}' not found"); - } - /** * @param string $id * @return self @@ -41,14 +31,20 @@ class Connections return $this; } + public function count(): int + { + return \count($this->connections); + } + /** * @return self * @throws \Exception */ public function reclaim(): self { - foreach ($this->connections as $connection) { + foreach ($this->connections as $id => $connection) { $connection->reclaim(); + unset($this->connections[$id]); } return $this; From 9a3f6e7f717ecfa2778814f730acac70e698887a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 9 Apr 2024 20:27:30 +1200 Subject: [PATCH 55/61] Add connections test --- tests/unit/Utopia/Pools/ConnectionsTest.php | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/unit/Utopia/Pools/ConnectionsTest.php diff --git a/tests/unit/Utopia/Pools/ConnectionsTest.php b/tests/unit/Utopia/Pools/ConnectionsTest.php new file mode 100644 index 0000000000..fb9c8bcc09 --- /dev/null +++ b/tests/unit/Utopia/Pools/ConnectionsTest.php @@ -0,0 +1,40 @@ +add($connection); + $this->assertEquals(1, $connections->count()); + } + + public function testRemove() + { + $connections = new Connections(); + $connection = new Connection('resource'); + $connections->add($connection); + $connections->remove($connection->getID()); + $this->assertEquals(0, $connections->count()); + } + + public function testReclaim() + { + $connections = new Connections(); + $pool = new Pool('test', 1, function () { + return 'resource'; + }); + $connection = $pool->pop(); + $connections->add($connection); + $connections->reclaim(); + $this->assertEquals(1, $pool->count()); + } +} From 3859f037a1885e54d6e08456288c804dc3aaba1c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 9 Apr 2024 20:50:33 +1200 Subject: [PATCH 56/61] Revert "Reclaim only used connections for workers" This reverts commit fefb60557df8a220bf1efc104e3adc867d3471b7. # Conflicts: # app/worker.php --- app/worker.php | 99 +++++++++++++++----------------------------------- 1 file changed, 30 insertions(+), 69 deletions(-) diff --git a/app/worker.php b/app/worker.php index 99f4154c98..32ce35e033 100644 --- a/app/worker.php +++ b/app/worker.php @@ -16,7 +16,6 @@ use Appwrite\Event\Phone; use Appwrite\Event\Usage; use Appwrite\Event\UsageDump; use Appwrite\Platform\Appwrite; -use Appwrite\Utopia\Pools\Connections; use Swoole\Runtime; use Utopia\App; use Utopia\Cache\Adapter\Sharding; @@ -37,32 +36,25 @@ use Utopia\Pools\Group; use Utopia\Queue\Connection; Authorization::disable(); -Runtime::enableCoroutine(); +Runtime::enableCoroutine(SWOOLE_HOOK_ALL); Server::setResource('register', fn () => $register); -Server::setResource('connections', function () { - return new Connections(); -}); - -Server::setResource('dbForConsole', function (Cache $cache, Registry $register, Connections $connections) { +Server::setResource('dbForConsole', function (Cache $cache, Registry $register) { $pools = $register->get('pools'); - - $connection = $pools + $database = $pools ->get('console') - ->pop(); - - $connections->add($connection); - - $database = $connection->getResource(); + ->pop() + ->getResource() + ; $adapter = new Database($database, $cache); $adapter->setNamespace('_console'); return $adapter; -}, ['cache', 'register', 'connections']); +}, ['cache', 'register']); -Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Database $dbForConsole, Connections $connections) { +Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Database $dbForConsole) { $payload = $message->getPayload() ?? []; $project = new Document($payload['project'] ?? []); @@ -71,19 +63,16 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register, } $pools = $register->get('pools'); - - $connection = $pools + $database = $pools ->get($project->getAttribute('database')) - ->pop(); - - $database = $connection->getResource(); - - $connections->add($connection); + ->pop() + ->getResource() + ; $adapter = new Database($database, $cache); $adapter->setNamespace('_' . $project->getInternalId()); return $adapter; -}, ['cache', 'register', 'message', 'dbForConsole', 'connections']); +}, ['cache', 'register', 'message', 'dbForConsole']); Server::setResource('project', function (Message $message, Database $dbForConsole) { $payload = $message->getPayload() ?? []; @@ -92,14 +81,14 @@ Server::setResource('project', function (Message $message, Database $dbForConsol if ($project->getId() === 'console') { return $project; } - return $dbForConsole->getDocument('projects', $project->getId()); + ; }, ['message', 'dbForConsole']); -Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, Cache $cache, Connections $connections) { +Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - return function (Document $project) use ($pools, $dbForConsole, $cache, $connections, &$databases): Database { + return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases): Database { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForConsole; } @@ -112,13 +101,10 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso return $database; } - $dbConnection = $pools + $dbAdapter = $pools ->get($databaseName) - ->pop(); - - $dbAdapter = $dbConnection->getResource(); - - $connections->add($dbConnection); + ->pop() + ->getResource(); $database = new Database($dbAdapter, $cache); @@ -128,7 +114,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso return $database; }; -}, ['pools', 'dbForConsole', 'cache', 'connections']); +}, ['pools', 'dbForConsole', 'cache']); Server::setResource('abuseRetention', function () { return DateTime::addSeconds(new \DateTime(), -1 * App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400)); @@ -142,104 +128,82 @@ Server::setResource('executionRetention', function () { return DateTime::addSeconds(new \DateTime(), -1 * App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); }); -Server::setResource('cache', function (Registry $register, Connections $connections) { +Server::setResource('cache', function (Registry $register) { $pools = $register->get('pools'); $list = Config::getParam('pools-cache', []); $adapters = []; foreach ($list as $value) { - $connection = $pools + $adapters[] = $pools ->get($value) - ->pop(); - - $connections->add($connection); - - $adapters[] = $connection->getResource(); + ->pop() + ->getResource() + ; } return new Cache(new Sharding($adapters)); -}, ['register', 'connections']); - +}, ['register']); Server::setResource('log', fn() => new Log()); - Server::setResource('queueForUsage', function (Connection $queue) { return new Usage($queue); }, ['queue']); - Server::setResource('queueForUsageDump', function (Connection $queue) { return new UsageDump($queue); }, ['queue']); - Server::setResource('queue', function (Group $pools) { return $pools->get('queue')->pop()->getResource(); }, ['pools']); - Server::setResource('queueForDatabase', function (Connection $queue) { return new EventDatabase($queue); }, ['queue']); - Server::setResource('queueForMessaging', function (Connection $queue) { return new Phone($queue); }, ['queue']); - Server::setResource('queueForMails', function (Connection $queue) { return new Mail($queue); }, ['queue']); - Server::setResource('queueForBuilds', function (Connection $queue) { return new Build($queue); }, ['queue']); - Server::setResource('queueForDeletes', function (Connection $queue) { return new Delete($queue); }, ['queue']); - Server::setResource('queueForEvents', function (Connection $queue) { return new Event($queue); }, ['queue']); - Server::setResource('queueForAudits', function (Connection $queue) { return new Audit($queue); }, ['queue']); - Server::setResource('queueForFunctions', function (Connection $queue) { return new Func($queue); }, ['queue']); - Server::setResource('queueForCertificates', function (Connection $queue) { return new Certificate($queue); }, ['queue']); - Server::setResource('queueForMigrations', function (Connection $queue) { return new Migration($queue); }, ['queue']); - Server::setResource('logger', function (Registry $register) { return $register->get('logger'); }, ['register']); - Server::setResource('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); - Server::setResource('getFunctionsDevice', function () { return function (string $projectId) { return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $projectId); }; }); - Server::setResource('getFilesDevice', function () { return function (string $projectId) { return getDevice(APP_STORAGE_UPLOADS . '/app-' . $projectId); }; }); - Server::setResource('getBuildsDevice', function () { return function (string $projectId) { return getDevice(APP_STORAGE_BUILDS . '/app-' . $projectId); }; }); - Server::setResource('getCacheDevice', function () { return function (string $projectId) { return getDevice(APP_STORAGE_CACHE . '/app-' . $projectId); @@ -290,9 +254,9 @@ $worker = $platform->getWorker(); $worker ->shutdown() - ->inject('connections') - ->action(function (Connections $connections) { - $connections->reclaim(); + ->inject('pools') + ->action(function (Group $pools) { + $pools->reclaim(); }); $worker @@ -301,10 +265,7 @@ $worker ->inject('logger') ->inject('log') ->inject('project') - ->inject('connections') - ->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Connections $connections) { - $connections->reclaim(); - + ->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project) { $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); if ($error instanceof PDOException) { From 8cad8f5533345fdfa76cad0e68a0b02865522422 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 10 Apr 2024 19:46:31 +0200 Subject: [PATCH 57/61] fix: admin mode on console --- app/controllers/general.php | 10 ++++++++++ tests/e2e/General/ModeTest.php | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/e2e/General/ModeTest.php diff --git a/app/controllers/general.php b/app/controllers/general.php index 434117846b..d5fecac81e 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -367,6 +367,16 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo return false; } +App::init() + ->groups(['api']) + ->inject('project') + ->inject('mode') + ->action(function(Document $project, string $mode) { + if ($mode === APP_MODE_ADMIN && $project->getId() === 'console') { + throw new AppwriteException(AppwriteException::GENERAL_BAD_REQUEST, 'Admin mode is not allowed for console project'); + } + }); + App::init() ->groups(['api', 'web']) ->inject('utopia') diff --git a/tests/e2e/General/ModeTest.php b/tests/e2e/General/ModeTest.php new file mode 100644 index 0000000000..ce0852a586 --- /dev/null +++ b/tests/e2e/General/ModeTest.php @@ -0,0 +1,32 @@ +client->call(Client::METHOD_GET, '/account', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-mode' => 'admin', + ], $this->getHeaders())); + + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals(Exception::GENERAL_BAD_REQUEST, $response['body']['type']); + } +} From 11fdefa2b81ec9a44a8e287049f1161ff2e55ef0 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 10 Apr 2024 19:49:16 +0200 Subject: [PATCH 58/61] chore: run formatter --- app/controllers/general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index d5fecac81e..a25e4826b9 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -371,7 +371,7 @@ App::init() ->groups(['api']) ->inject('project') ->inject('mode') - ->action(function(Document $project, string $mode) { + ->action(function (Document $project, string $mode) { if ($mode === APP_MODE_ADMIN && $project->getId() === 'console') { throw new AppwriteException(AppwriteException::GENERAL_BAD_REQUEST, 'Admin mode is not allowed for console project'); } From 7cff879e58e08e8f3e335b0226aed22a0f5782bc Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 10 Apr 2024 19:52:14 +0200 Subject: [PATCH 59/61] style: remove unused imports --- tests/e2e/General/ModeTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/General/ModeTest.php b/tests/e2e/General/ModeTest.php index ce0852a586..ceafbc4b3a 100644 --- a/tests/e2e/General/ModeTest.php +++ b/tests/e2e/General/ModeTest.php @@ -5,10 +5,8 @@ namespace Tests\E2E\Services\Console; use Appwrite\Extend\Exception; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectConsole; -use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; -use Tests\E2E\Scopes\SideConsole; class ModeTest extends Scope { From 114a450f26bf550e1629a7171cc8b9985bf6a445 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Apr 2024 14:18:31 +1200 Subject: [PATCH 60/61] Update websockets to release version (no change) --- composer.json | 2 +- composer.lock | 21 ++++++++----------- .../Console}/ModeTest.php | 0 3 files changed, 10 insertions(+), 13 deletions(-) rename tests/e2e/{General => Services/Console}/ModeTest.php (100%) diff --git a/composer.json b/composer.json index 42eedfaba6..c1ac3dac4f 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/storage": "0.18.*", "utopia-php/swoole": "0.5.*", "utopia-php/vcs": "0.6.*", - "utopia-php/websocket": "dev-feat-worker-stop", + "utopia-php/websocket": "0.2.0", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", "phpmailer/phpmailer": "6.8.0", diff --git a/composer.lock b/composer.lock index 70fe96499c..a7904ca600 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "ce7a7f429d68836b8667e2b02440fc3f", + "content-hash": "7ec104df6534b79c176a50541bf5ca24", "packages": [ { "name": "adhocore/jwt", @@ -2273,16 +2273,16 @@ }, { "name": "utopia-php/websocket", - "version": "dev-feat-worker-stop", + "version": "0.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/websocket.git", - "reference": "b7adfab69d4c48a60272fa4cb3328f9b400b0ffa" + "reference": "e9d0919b321744a61f12563f5791c47ba9f57810" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/websocket/zipball/b7adfab69d4c48a60272fa4cb3328f9b400b0ffa", - "reference": "b7adfab69d4c48a60272fa4cb3328f9b400b0ffa", + "url": "https://api.github.com/repos/utopia-php/websocket/zipball/e9d0919b321744a61f12563f5791c47ba9f57810", + "reference": "e9d0919b321744a61f12563f5791c47ba9f57810", "shasum": "" }, "require": { @@ -2292,9 +2292,8 @@ "laravel/pint": "^1.15", "phpstan/phpstan": "^1.8", "phpunit/phpunit": "^9.5.5", - "swoole/ide-helper": "4.6.6", + "swoole/ide-helper": "5.1.2", "textalk/websocket": "1.5.2", - "vimeo/psalm": "^4.8.1", "workerman/workerman": "^4.0" }, "type": "library", @@ -2317,9 +2316,9 @@ ], "support": { "issues": "https://github.com/utopia-php/websocket/issues", - "source": "https://github.com/utopia-php/websocket/tree/feat-worker-stop" + "source": "https://github.com/utopia-php/websocket/tree/0.2.0" }, - "time": "2024-04-09T04:48:45+00:00" + "time": "2024-04-09T08:28:11+00:00" }, { "name": "webmozart/assert", @@ -5137,9 +5136,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/websocket": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/tests/e2e/General/ModeTest.php b/tests/e2e/Services/Console/ModeTest.php similarity index 100% rename from tests/e2e/General/ModeTest.php rename to tests/e2e/Services/Console/ModeTest.php From ff8ebb4f7ebdf24314d1f0c055c680ddb1f6890e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Apr 2024 19:09:04 +1200 Subject: [PATCH 61/61] Match memberships on internal ID --- app/controllers/general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index a25e4826b9..d792587c5d 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -640,7 +640,7 @@ App::init() : Role::users()->toString(); // Add user roles - $memberships = $user->find('teamId', $project->getAttribute('teamId'), 'memberships'); + $memberships = $user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships'); if ($memberships) { foreach ($memberships->getAttribute('roles', []) as $memberRole) {