diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index d4507ff561..100ce65e7c 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -2,6 +2,7 @@ use Appwrite\Auth\Auth; use Appwrite\Auth\Validator\Password; +use Appwrite\Database\DatabasePool; use Appwrite\Event\Certificate; use Appwrite\Event\Delete; use Appwrite\Event\Validator\Event; @@ -10,6 +11,8 @@ use Appwrite\Network\Validator\Domain as DomainValidator; use Appwrite\Network\Validator\Origin; use Appwrite\Network\Validator\URL; use Appwrite\Utopia\Database\Validator\CustomId; +use Utopia\Cache\Cache; +use Utopia\Cache\Adapter\Redis as RedisCache; use Appwrite\Utopia\Response; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\App; @@ -23,6 +26,7 @@ use Utopia\Database\Validator\UID; use Utopia\Domains\Domain; use Utopia\Registry\Registry; use Appwrite\Extend\Exception; +use Utopia\Database\Adapter\MariaDB; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; use Utopia\Validator\Hostname; @@ -61,8 +65,9 @@ App::post('/v1/projects') ->param('legalTaxId', '', new Text(256), 'Project legal Tax ID. Max length: 256 chars.', true) ->inject('response') ->inject('dbForConsole') - ->inject('dbForProject') - ->action(function (string $projectId, string $name, string $teamId, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole, Database $dbForProject) { + ->inject('cache') + ->inject('dbPool') + ->action(function (string $projectId, string $name, string $teamId, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole, Redis $cache, DatabasePool $dbPool) { $team = $dbForConsole->getDocument('teams', $teamId); @@ -80,6 +85,9 @@ App::post('/v1/projects') if ($projectId === 'console') { throw new Exception("'console' is a reserved project.", 400, Exception::PROJECT_RESERVED_PROJECT); } + + ['name' => $dbName, 'db' => $db] = $dbPool->getAny(); + $project = $dbForConsole->createDocument('projects', new Document([ '$id' => $projectId == 'unique()' ? $dbForConsole->getId() : $projectId, '$read' => ['team:' . $teamId], @@ -104,12 +112,13 @@ App::post('/v1/projects') 'domains' => null, 'auths' => $auths, 'search' => implode(' ', [$projectId, $name]), - 'database' + 'database' => $dbName ])); - /** @var array $collections */ - $collections = Config::getParam('collections', []); - $dbForProject->setNamespace("_{$project->getId()}"); + $cache = new Cache(new RedisCache($cache)); + $dbForProject = new Database(new MariaDB($db), $cache); + $dbForProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); + $dbForProject->setNamespace("_{$projectId}"); $dbForProject->create('appwrite'); $audit = new Audit($dbForProject); @@ -118,6 +127,9 @@ App::post('/v1/projects') $adapter = new TimeLimit('', 0, 1, $dbForProject); $adapter->setup(); + /** @var array $collections */ + $collections = Config::getParam('collections', []); + foreach ($collections as $key => $collection) { if (($collection['$collection'] ?? '') !== Database::METADATA) { continue; @@ -152,6 +164,8 @@ App::post('/v1/projects') $dbForProject->createCollection($key, $attributes, $indexes); } + $dbPool->put($db, $dbName); + $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($project, Response::MODEL_PROJECT); }); diff --git a/app/http.php b/app/http.php index 5a49e49e81..550aa05c71 100644 --- a/app/http.php +++ b/app/http.php @@ -17,6 +17,9 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Swoole\Files; use Appwrite\Utopia\Request; +use Utopia\Cache\Cache; +use Utopia\Cache\Adapter\Redis as RedisCache; +use Utopia\Database\Adapter\MariaDB; use Utopia\Logger\Log; use Utopia\Logger\Log\User; @@ -66,7 +69,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { do { try { $attempts++; - $pool = $register->get('poolForConsole')->get(); + $db = $register->get('dbPool')->getConsoleDB(); $redis = $register->get('redisPool')->get(); break; // leave the do-while if successful } catch (\Exception $e) { @@ -239,12 +242,33 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $app = new App('UTC'); - $db = $register->get('dbPool')->get(); + $dbPool = $register->get('dbPool'); + $db = $dbPool->getConsoleDB(); $redis = $register->get('redisPool')->get(); App::setResource('db', fn() => $db); + App::setResource('dbPool', fn() => $dbPool); App::setResource('cache', fn() => $redis); + $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console')); + $projectDB = $db; + if ($projectId !== 'console') { + $dbForConsole = $app->getResource('dbForConsole'); /** @var Utopia\Database\Database $dbForConsole */ + $project = Authorization::skip(fn() => $dbForConsole->getDocument('projects', $projectId)); + $dbName = $project->getAttribute('database', ''); + if (!empty($dbName)) { + $projectDB = $register->get('dbPool')->get($dbName); + } + } + + App::setResource('dbForProject', function ($cache) use ($projectDB, $projectId) { + $cache = new Cache(new RedisCache($cache)); + $database = new Database(new MariaDB($projectDB), $cache); + $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); + $database->setNamespace("_{$projectId}"); + return $database; + }, ['cache']); + try { Authorization::cleanRoles(); Authorization::setRole('role:all'); @@ -334,9 +358,12 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $swooleResponse->end(\json_encode($output)); } finally { - /** @var PDOPool $dbPool */ - $dbPool = $register->get('dbPool'); - $dbPool->put($db); + /** @var PDOPool $consolePool */ + $dbPool->putConsoleDb($db); + + if (!empty($dbName) && !empty($projectDB)) { + $dbPool->put($projectDB, $dbName); + } /** @var RedisPool $redisPool */ $redisPool = $register->get('redisPool'); diff --git a/app/init.php b/app/init.php index 8910fc34b3..6c7ebd54b2 100644 --- a/app/init.php +++ b/app/init.php @@ -445,80 +445,43 @@ $register->set('logger', function () { return new Logger($adapter); }); -$register->set('poolForConsole', function () { - $dbs = App::getEnv('_APP_CONSOLE_DB', ''); - $dbs = explode(',', $dbs); +$register->set('dbPool', function () { + /** Parse the console databases */ + $consoleDb = App::getEnv('_APP_CONSOLE_DB', ''); + $consoleDb = explode(',', $consoleDb)[0]; + $consoleDb = explode('=', $consoleDb); + $name = $consoleDb[0]; + $dsn = new DSN($consoleDb[1]); - $pools = new DatabasePool(); - foreach ($dbs as $db) { - $db = explode('=', $db); - $name = $db[0]; - $dsn = new DSN($db[1]); + /** Create a new Database Pool */ + $pool = new DatabasePool(); - // var_dump($dsn->getHost(), $dsn->getPort(), $dsn->getDatabase(), $dsn->getUser(), $dsn->getPassword()); + $consolePool = new PDOPool( + (new PDOConfig()) + ->withHost($dsn->getHost()) + ->withPort($dsn->getPort()) + ->withDbName($dsn->getDatabase()) + ->withCharset('utf8mb4') + ->withUsername($dsn->getUser()) + ->withPassword($dsn->getPassword()) + ->withOptions([ + PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed + ]), + 64 + ); - $pool = new PDOPool( - (new PDOConfig()) - ->withHost($dsn->getHost()) - ->withPort($dsn->getPort()) - ->withDbName($dsn->getDatabase()) - ->withCharset('utf8mb4') - ->withUsername($dsn->getUser()) - ->withPassword($dsn->getPassword()) - ->withOptions([ - PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed - ]), - 64 - ); - - $pools->add($name, $pool); - } - - return $pools; -}); - -$register->set('poolForProject', function () { - // Register DB connection - // $dbHost = App::getEnv('_APP_DB_HOST', ''); - // $dbPort = App::getEnv('_APP_DB_PORT', ''); - // $dbUser = App::getEnv('_APP_DB_USER', ''); - // $dbPass = App::getEnv('_APP_DB_PASS', ''); - // $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); - - // var_dump($dbHost, $dbPort, $dbScheme, $dbUser, $dbPass); - // $pool = new PDOPool( - // (new PDOConfig()) - // ->withHost($dbHost) - // ->withPort($dbPort) - // ->withDbName($dbScheme) - // ->withCharset('utf8mb4') - // ->withUsername($dbUser) - // ->withPassword($dbPass) - // ->withOptions([ - // PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed - // ]), - // 64 - // ); - - // return $pool; - - // $dbForConsole = App::getEnv('_APP_CONSOLE_DB', ''); - // $dbForConsole = explode('=', $dbForConsole); - // $name = $dbForConsole[0]; - // $dsn = new DSN($dbForConsole[1]); + $pool->add($name, $consolePool); + $pool->setConsoleDB($name); + /** Parse the project databases */ $dbs = App::getEnv('_APP_PROJECT_DB', ''); $dbs = explode(',', $dbs); - - $pools = new DatabasePool(); foreach ($dbs as $db) { $db = explode('=', $db); $name = $db[0]; $dsn = new DSN($db[1]); - // var_dump($dsn->getHost(), $dsn->getPort(), $dsn->getDatabase(), $dsn->getUser(), $dsn->getPassword()); - - $pool = new PDOPool( + $projectPool = new PDOPool( (new PDOConfig()) ->withHost($dsn->getHost()) ->withPort($dsn->getPort()) @@ -532,11 +495,12 @@ $register->set('poolForProject', function () { 64 ); - $pools->add($name, $pool); + $pool->add($name, $projectPool); } - return $pools; + return $pool; }); + $register->set('redisPool', function () { $redisHost = App::getEnv('_APP_REDIS_HOST', ''); $redisPort = App::getEnv('_APP_REDIS_PORT', ''); diff --git a/src/Appwrite/Database/DatabasePool.php b/src/Appwrite/Database/DatabasePool.php index 6678ebd20a..e49226ff77 100644 --- a/src/Appwrite/Database/DatabasePool.php +++ b/src/Appwrite/Database/DatabasePool.php @@ -2,36 +2,127 @@ namespace Appwrite\Database; -use PDO; use Appwrite\Extend\Exception; use Swoole\Database\PDOPool; use Swoole\Database\PDOProxy; class DatabasePool { + /** + * @var array + */ protected array $pools = []; + /** + * @var string + */ + protected string $consoleDB = ''; + + /** + * Function to get the name of the console database. + * + * @return ?PDOProxy + */ + public function getConsoleDB(): ?PDOProxy + { + if (empty($this->consoleDB)) { + throw new Exception("Console DB not set", 500); + } + + return $this->get($this->consoleDB); + } + + /** + * Return a PDO instance back to the console database pool + * + * @param PDOProxy $db + * + * @return void + */ + public function putConsoleDb(PDOProxy $db): void + { + $this->put($db, $this->consoleDB); + } + + /** + * Function to set the name of the console database + * + * @param string $consoleDB + * + * @return void + */ + public function setConsoleDB(string $consoleDB): void + { + if(!isset($this->pools[$consoleDB])) { + throw new Exception("Console DB with name : $consoleDB not found. Add it using ", 500); + } + + $this->consoleDB = $consoleDB; + } + + /** + * Add a new PDOPool into the list of available pools + * + * @param string $name + * @param PDOPool $dbPool + * + * @return void + */ public function add(string $name, PDOPool $dbPool): void { $this->pools[$name] = $dbPool; } - public function get(string $name = 'console'): ?PDOProxy + /** + * Get a PDO instance from the list of available database pools + * + * @param string $name + * + * @return ?PDOProxy + */ + public function get(string $name): ?PDOProxy { $pool = $this->pools[$name] ?? null; if ($pool === null) { - throw new Exception("Database Pool with name : $name not found. Please check the value of _APP_PROJECT_DB in .env", 500); + throw new Exception("Database pool with name : $name not found. Check the value of _APP_PROJECT_DB in .env", 500); } return $pool->get(); } - public function put(PDOProxy $db, string $name = 'console'): void + /** + * Function to get a random PDO instance from the available database pools database + * + * @return array [PDO, string] + */ + public function getAny(): ?array + { + if (count($this->pools) === 0) { + throw new Exception("No database pools found. Add pools using DatabasePool::add() method", 500); + } + + $key = array_rand($this->pools); + $pool = $this->pools[$key] ?? null; + + return [ + 'name' => $key, + 'db' => $pool ? $pool->get() : null + ]; + } + + /** + * Return a PDO instance back to its database pool + * + * @param PDOProxy $db + * @param string $name + * + * @return void + */ + public function put(PDOProxy $db, string $name): void { $pool = $this->pools[$name] ?? null; if ($pool === null) { - throw new Exception("Database Pool with name : $name not found. Cannot put", 500); + throw new Exception("Failed to put PDO into database pool. Database pool with name : $name not found", 500); } $pool->put($db); } - } \ No newline at end of file