appwrite/src/Appwrite/Database/DatabasePool.php

338 lines
10 KiB
PHP
Raw Normal View History

<?php
namespace Appwrite\Database;
2022-07-01 12:18:33 +00:00
use Appwrite\DSN\DSN;
use Appwrite\Extend\Exception;
2022-07-01 12:18:33 +00:00
use PDO;
use Swoole\Database\PDOConfig;
use Swoole\Database\PDOPool;
use Swoole\Database\PDOProxy;
2022-07-01 12:18:33 +00:00
use Utopia\App;
2022-07-09 13:41:14 +00:00
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
class DatabasePool {
2022-06-23 08:50:00 +00:00
/**
* @var array
2022-07-01 12:18:33 +00:00
*
* Array to store mappings from database names to PDOPool instances.
2022-06-23 08:50:00 +00:00
*/
protected array $pools = [];
2022-07-01 12:18:33 +00:00
/**
* @var array
*
* Array to store mappings from database names to DSNs
*/
2022-07-09 13:41:14 +00:00
protected array $dsns = [];
2022-07-01 12:18:33 +00:00
2022-06-23 08:50:00 +00:00
/**
* @var string
2022-07-09 13:41:14 +00:00
*
* The name of the console Database
2022-06-23 08:50:00 +00:00
*/
protected string $consoleDB = '';
/**
2022-07-01 12:18:33 +00:00
* Constructor for Database pools
*
* @param array $consoleDB
* @param array $projectDB
2022-06-23 08:50:00 +00:00
*
*/
2022-07-01 12:18:33 +00:00
public function __construct(array $consoleDB, array $projectDB)
2022-06-23 08:50:00 +00:00
{
2022-07-01 12:18:33 +00:00
if(count($consoleDB) != 1) {
throw new Exception('Console DB should contain only one entry', 500);
2022-06-23 08:50:00 +00:00
}
2022-07-01 12:18:33 +00:00
if(empty($projectDB)) {
throw new Exception('Project DB is not defined', 500);
}
2022-06-23 08:50:00 +00:00
2022-07-01 12:18:33 +00:00
$this->consoleDB = array_key_first($consoleDB);
2022-07-09 13:41:14 +00:00
$this->dsns = array_merge($consoleDB, $projectDB);
2022-07-01 12:18:33 +00:00
2022-07-09 13:41:14 +00:00
/** Create PDO pool instances for all the dsns */
foreach ($this->dsns as $name => $dsn) {
2022-07-01 12:18:33 +00:00
$dsn = new DSN($dsn);
$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
2022-07-15 09:54:27 +00:00
PDO::ATTR_TIMEOUT => 3, // Seconds
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => true,
PDO::ATTR_STRINGIFY_FETCHES => true
2022-07-01 12:18:33 +00:00
]),
64
);
2022-07-09 13:41:14 +00:00
2022-07-01 12:18:33 +00:00
$this->pools[$name] = $pool;
}
2022-06-23 08:50:00 +00:00
}
/**
2022-07-09 13:41:14 +00:00
* Get a PDO instance by database name
2022-06-23 08:50:00 +00:00
*
2022-07-01 12:18:33 +00:00
* @param string $name
* @return ?PDO
2022-06-23 08:50:00 +00:00
*/
2022-07-15 09:54:27 +00:00
public function getPDO(string $name): ?PDO
2022-06-23 08:50:00 +00:00
{
2022-07-09 13:41:14 +00:00
$dsn = $this->dsns[$name] ?? throw new Exception("Database with name : $name not found.", 500);
2022-07-01 12:18:33 +00:00
$dsn = new DSN($dsn);
$dbHost = $dsn->getHost();
$dbPort = $dsn->getPort();
$dbUser = $dsn->getUser();
$dbPass = $dsn->getPassword();
$dbScheme = $dsn->getDatabase();
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
PDO::ATTR_TIMEOUT => 3, // Seconds
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
));
return $pdo;
2022-06-23 08:50:00 +00:00
}
/**
2022-07-09 13:41:14 +00:00
* @param string $projectID
2022-06-23 08:50:00 +00:00
*
2022-07-09 13:41:14 +00:00
* @return string
2022-07-01 12:18:33 +00:00
*
2022-07-09 13:41:14 +00:00
* Function to return the name of the database from the project ID
2022-06-23 08:50:00 +00:00
*/
2022-07-14 00:11:35 +00:00
private function getName(string $projectID, \Redis $redis): array
{
2022-07-09 13:41:14 +00:00
if ($projectID === 'console') {
2022-07-14 00:11:35 +00:00
return [$this->consoleDB, 'console'];
2022-07-09 13:41:14 +00:00
}
$pdo = $this->getPDO($this->consoleDB);
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($pdo), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
2022-07-15 20:19:50 +00:00
$namespace = "_console";
2022-07-15 09:54:27 +00:00
$database->setNamespace($namespace);
2022-07-09 13:41:14 +00:00
$project = Authorization::skip(fn() => $database->getDocument('projects', $projectID));
2022-07-14 00:11:35 +00:00
$internalID = $project->getInternalId();
2022-07-09 13:41:14 +00:00
$database = $project->getAttribute('database', '');
2022-07-14 00:11:35 +00:00
return [$database, $internalID];
}
2022-06-23 08:50:00 +00:00
/**
2022-07-09 13:41:14 +00:00
* Get a single PDO instance for a project
2022-06-23 08:50:00 +00:00
*
2022-07-09 13:41:14 +00:00
* @param string $projectId
*
* @return ?Database
2022-06-23 08:50:00 +00:00
*/
2022-07-15 09:54:27 +00:00
public function getDB(string $projectID, ?\Redis $redis): ?Database
{
2022-07-09 13:41:14 +00:00
/** Get DB name from the console database */
2022-07-14 00:11:35 +00:00
[$name, $internalID] = $this->getName($projectID, $redis);
2022-07-09 13:41:14 +00:00
if (empty($name)) {
throw new Exception("Database with name : $name not found.", 500);
}
2022-07-09 13:41:14 +00:00
/** Get a PDO instance using the databse name */
$pdo = $this->getPDO($name);
2022-07-13 23:33:25 +00:00
$cache = new Cache(new RedisCache($redis));
2022-07-09 13:41:14 +00:00
$database = new Database(new MariaDB($pdo), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
2022-07-15 20:19:50 +00:00
$namespace = "_$internalID";
2022-07-15 09:54:27 +00:00
$database->setNamespace($namespace);
2022-07-09 13:41:14 +00:00
return $database;
}
2022-07-09 13:41:14 +00:00
// private function attemptConnection(PDO|PDOProxy $pdo, ?string $namespace, \Redis $cache): Database
// {
// }
2022-06-23 08:50:00 +00:00
/**
2022-07-09 13:41:14 +00:00
* Get a PDO instance from the list of available database pools . Meant to be used in co-routines
2022-06-23 08:50:00 +00:00
*
2022-07-09 13:41:14 +00:00
* @param string $projectId
*
* @return array
2022-06-23 08:50:00 +00:00
*/
2022-07-09 13:41:14 +00:00
public function getDBFromPool(string $projectID, \Redis $redis): array
2022-06-23 08:50:00 +00:00
{
2022-07-09 13:41:14 +00:00
/** Get DB name from the console database */
2022-07-14 00:11:35 +00:00
[$name, $internalID] = $this->getName($projectID, $redis);
2022-07-09 13:41:14 +00:00
$pool = $this->pools[$name] ?? throw new Exception("Database pool with name : $name not found. Check the value of _APP_PROJECT_DB in .env", 500);
2022-07-15 20:19:50 +00:00
$namespace = "_$internalID";
2022-07-09 13:41:14 +00:00
$attempts = 0;
do {
try {
$attempts++;
$pdo = $pool->get();
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($pdo), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace($namespace);
// if (!$database->exists($database->getDefaultDatabase(), 'metadata')) {
// throw new Exception('Collection not ready');
// }
break; // leave loop if successful
} catch (\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep(DATABASE_RECONNECT_SLEEP);
}
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
2022-07-01 12:18:33 +00:00
2022-06-23 08:50:00 +00:00
return [
2022-07-09 13:41:14 +00:00
$database,
function () use ($pdo, $name) {
$this->put($pdo, $name);
}
2022-06-23 08:50:00 +00:00
];
}
2022-07-09 13:41:14 +00:00
/**
* Function to get a random PDO instance from the available database pools
2022-07-01 12:18:33 +00:00
*
2022-07-09 13:41:14 +00:00
* @return array [PDO, string]
2022-07-01 12:18:33 +00:00
*/
2022-07-09 13:41:14 +00:00
public function getAnyFromPool(\Redis $redis): array
2022-07-01 12:18:33 +00:00
{
2022-07-09 13:41:14 +00:00
$name = array_rand($this->pools);
$pool = $this->pools[$name] ?? throw new Exception("Database pool with name : $name not found. Check the value of _APP_PROJECT_DB in .env", 500);
2022-07-01 12:18:33 +00:00
2022-07-09 13:41:14 +00:00
$attempts = 0;
do {
try {
$attempts++;
$pdo = $pool->get();
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($pdo), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
2022-07-01 12:18:33 +00:00
2022-07-09 13:41:14 +00:00
// if (!$database->exists($database->getDefaultDatabase(), 'metadata')) {
// throw new Exception('Collection not ready');
// }
break; // leave loop if successful
} catch (\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep(DATABASE_RECONNECT_SLEEP);
}
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
2022-07-01 12:18:33 +00:00
2022-07-09 13:41:14 +00:00
return [
$database,
function () use ($pdo, $name) {
$this->put($pdo, $name);
},
$name
];
2022-07-01 12:18:33 +00:00
}
/**
2022-07-09 13:41:14 +00:00
* Return a PDO instance back to its database pool
2022-06-23 08:50:00 +00:00
*
* @param PDOProxy $db
2022-07-09 13:41:14 +00:00
* @param string $name
2022-06-23 08:50:00 +00:00
*
* @return void
*/
2022-07-09 13:41:14 +00:00
public function put(PDOProxy $db, string $name): void
{
2022-07-09 13:41:14 +00:00
$pool = $this->pools[$name] ?? null;
if ($pool === null) {
throw new Exception("Failed to put PDO into database pool. Database pool with name : $name not found", 500);
}
$pool->put($db);
2022-07-01 12:18:33 +00:00
}
2022-07-09 13:41:14 +00:00
// /**
// * Convenience methods for console DB
// */
2022-07-15 09:54:27 +00:00
/**
* Function to get the name of the console DB
*
* @return ?string
*/
public function getConsoleDB(): ?string
{
if (empty($this->consoleDB)) {
throw new Exception('Console DB is not defined', 500);
};
2022-07-09 13:41:14 +00:00
2022-07-15 09:54:27 +00:00
return $this->consoleDB;
}
2022-07-09 13:41:14 +00:00
// /**
// * Function to get an instance of the console DB from the database pool
// *
// * @return ?PDOProxy
// */
// public function getConsoleDBFromPool(): ?PDOProxy
// {
// if (empty($this->consoleDB)) {
// throw new Exception("Console DB not set", 500);
// }
// return $this->getDBFromPool($this->consoleDB);
// }
// /**
// * Return the console DB 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);
// }
2022-07-01 12:18:33 +00:00
2022-07-09 13:41:14 +00:00
// $this->consoleDB = $consoleDB;
// }
}