From 8acffb49af72a0a3bcf1f9bc969350bf4fdaf6f1 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Fri, 13 Sep 2024 11:33:26 +1200 Subject: [PATCH] NEW Allow database read-only replicas --- src/Core/CoreKernel.php | 81 ++++++++++++++++++++++++++++++++++------- src/ORM/DB.php | 10 ++++- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/Core/CoreKernel.php b/src/Core/CoreKernel.php index 7f968c8a3f4..4389797c94b 100644 --- a/src/Core/CoreKernel.php +++ b/src/Core/CoreKernel.php @@ -98,20 +98,58 @@ protected function bootDatabaseEnvVars() $databaseConfig = $this->getDatabaseConfig(); $databaseConfig['database'] = $this->getDatabaseName(); DB::setConfig($databaseConfig); + + // Set database replicas config + for ($i = 1; $i <= 99; $i++) { + $replicaKey = $this->getReplicaKey('SS_DATABASE_SERVER', $i); + if (!Environment::hasEnv($replicaKey)) { + break; + } + $replicaDatabaseConfig = $this->getDatabaseReplicaConfig($i); + $name = 'replica_' . str_pad($i, 2, '0', STR_PAD_LEFT); + DB::setConfig($replicaDatabaseConfig, $name); + } } /** - * Load database config from environment - * - * @return array + * Convert a database key to a replica key + * e.g. SS_DATABASE_SERVER -> SS_DATABASE_SERVER_REPLICA_01 */ - protected function getDatabaseConfig() + private function getReplicaKey(string $key, int $replica): string + { + // Left pad replica number with a zero if it's less than 10 + return $key . '_REPLICA_' . str_pad($replica, 2, '0', STR_PAD_LEFT); + } + + /** + * Reads a single database configuration variable from the environment + * For replica databases, it will first attempt to find replica-specific configuration + * before falling back to the default configuration. + * + * Replicate specific configuration has `_REPLICA_01` appended to the key + * where 01 is the replica number. + */ + private function getDatabaseConfigVariable(string $key, int $replica): string + { + if ($replica > 0) { + $replicaKey = $this->getReplicaKey($key, $replica); + if (Environment::hasEnv($replicaKey)) { + return Environment::getEnv($replicaKey); + } + } + if (Environment::hasEnv($key)) { + return Environment::getEnv($key); + } + return ''; + } + + private function getSingleDataBaseConfig(int $replica): array { $databaseConfig = [ - "type" => Environment::getEnv('SS_DATABASE_CLASS') ?: 'MySQLDatabase', - "server" => Environment::getEnv('SS_DATABASE_SERVER') ?: 'localhost', - "username" => Environment::getEnv('SS_DATABASE_USERNAME') ?: null, - "password" => Environment::getEnv('SS_DATABASE_PASSWORD') ?: null, + "type" => $this->getDatabaseConfigVariable('SS_DATABASE_CLASS', $replica) ?: 'MySQLDatabase', + "server" => $this->getDatabaseConfigVariable('SS_DATABASE_SERVER', $replica) ?: 'localhost', + "username" => $this->getDatabaseConfigVariable('SS_DATABASE_USERNAME', $replica) ?: null, + "password" => $this->getDatabaseConfigVariable('SS_DATABASE_PASSWORD', $replica) ?: null, ]; // Only add SSL keys in the array if there is an actual value associated with them @@ -122,7 +160,7 @@ protected function getDatabaseConfig() 'ssl_cipher' => 'SS_DATABASE_SSL_CIPHER', ]; foreach ($sslConf as $key => $envVar) { - $envValue = Environment::getEnv($envVar); + $envValue = $this->getDatabaseConfigVariable($envVar, $replica); if ($envValue) { $databaseConfig[$key] = $envValue; } @@ -138,25 +176,25 @@ protected function getDatabaseConfig() } // Set the port if called for - $dbPort = Environment::getEnv('SS_DATABASE_PORT'); + $dbPort = $this->getDatabaseConfigVariable('SS_DATABASE_PORT', $replica); if ($dbPort) { $databaseConfig['port'] = $dbPort; } // Set the timezone if called for - $dbTZ = Environment::getEnv('SS_DATABASE_TIMEZONE'); + $dbTZ = $this->getDatabaseConfigVariable('SS_DATABASE_TIMEZONE', $replica); if ($dbTZ) { $databaseConfig['timezone'] = $dbTZ; } // For schema enabled drivers: - $dbSchema = Environment::getEnv('SS_DATABASE_SCHEMA'); + $dbSchema = $this->getDatabaseConfigVariable('SS_DATABASE_SCHEMA', $replica); if ($dbSchema) { $databaseConfig["schema"] = $dbSchema; } // For SQlite3 memory databases (mainly for testing purposes) - $dbMemory = Environment::getEnv('SS_DATABASE_MEMORY'); + $dbMemory = $this->getDatabaseConfigVariable('SS_DATABASE_MEMORY', $replica); if ($dbMemory) { $databaseConfig["memory"] = $dbMemory; } @@ -166,6 +204,22 @@ protected function getDatabaseConfig() return $databaseConfig; } + /** + * Load database config from environment + * + * @return array + */ + protected function getDatabaseConfig() + { + return $this->getSingleDataBaseConfig(0); + } + + protected function getDatabaseReplicaConfig(int $replica) + { + $replica = 1; + return $this->getSingleDataBaseConfig($replica); + } + /** * @return string */ @@ -184,6 +238,7 @@ protected function getDatabaseSuffix() /** * Get name of database + * Note that any replicas must have the same database name * * @return string */ diff --git a/src/ORM/DB.php b/src/ORM/DB.php index 054b858d02f..a036b2fd463 100644 --- a/src/ORM/DB.php +++ b/src/ORM/DB.php @@ -311,11 +311,19 @@ public static function setConfig($databaseConfig, $name = 'default') */ public static function getConfig($name = 'default') { - if (isset(static::$configs[$name])) { + if (static::hasConfig($name)) { return static::$configs[$name]; } } + /** + * Check if a named connection config exists + */ + public static function hasConfig($name = 'default'): bool + { + return isset(static::$configs[$name]); + } + /** * Returns true if a database connection has been attempted. * In particular, it lets the caller know if we're still so early in the execution pipeline that