From 79c4986354da2c7dd102de21174114e7687daf98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Sun, 3 Dec 2023 09:14:55 +0100 Subject: [PATCH] enh: Implement PrimaryReadReplicaConnection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- config/config.sample.php | 8 ++++++++ lib/private/DB/Connection.php | 19 +++++++++++++++++-- lib/private/DB/ConnectionFactory.php | 18 +++++++++--------- .../DB/SetTransactionIsolationLevel.php | 10 +++++++++- lib/private/Server.php | 3 +-- lib/private/Setup/AbstractDatabase.php | 2 +- 6 files changed, 45 insertions(+), 15 deletions(-) diff --git a/config/config.sample.php b/config/config.sample.php index a1f7332c4047b..bafe999743ba9 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -151,6 +151,14 @@ */ 'dbpersistent' => '', +/** + * Specify read only replicas to be used by Nextcloud when querying the database + */ +'dbreplica' => [ + ['user' => 'replica1', 'password', 'host' => '', 'dbname' => ''], + ['user' => 'replica1', 'password', 'host' => '', 'dbname' => ''], +], + /** * Indicates whether the Nextcloud instance was installed successfully; ``true`` * indicates a successful installation, and ``false`` indicates an unsuccessful diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 6e2724ca5abc1..e3fda3e464fdb 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -38,6 +38,7 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\DBAL\Configuration; +use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\MySQLPlatform; @@ -55,7 +56,7 @@ use OCP\Profiler\IProfiler; use Psr\Log\LoggerInterface; -class Connection extends \Doctrine\DBAL\Connection { +class Connection extends PrimaryReadReplicaConnection { /** @var string */ protected $tablePrefix; @@ -119,7 +120,7 @@ public function __construct( /** * @throws Exception */ - public function connect() { + public function connect($connectionName = null) { try { if ($this->_conn) { /** @psalm-suppress InternalMethod */ @@ -302,6 +303,10 @@ protected function logQueryToFile(string $sql): void { $prefix .= \OC::$server->get(IRequestId::class)->getId() . "\t"; } + // FIXME: Improve to log the actual target db host + $isPrimary = $this->connections['primary'] === $this->_conn; + $prefix .= ' ' . ($isPrimary === true ? 'primary' : 'replica') . ' '; + file_put_contents( $this->systemConfig->getValue('query_log_file', ''), $prefix . $sql . "\n", @@ -603,4 +608,14 @@ private function getMigrator() { return new Migrator($this, $config, $dispatcher); } } + + protected function performConnect(?string $connectionName = null): bool { + $before = $this->isConnectedToPrimary(); + $result = parent::performConnect($connectionName); + $after = $this->isConnectedToPrimary(); + if (!$before && $after) { + $this->logger->debug('Switched to primary database', ['exception' => new \Exception()]); + } + return $result; + } } diff --git a/lib/private/DB/ConnectionFactory.php b/lib/private/DB/ConnectionFactory.php index 4b286ff5442ac..e868f18ec34c5 100644 --- a/lib/private/DB/ConnectionFactory.php +++ b/lib/private/DB/ConnectionFactory.php @@ -32,7 +32,6 @@ use Doctrine\DBAL\Configuration; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Event\Listeners\OracleSessionInit; -use Doctrine\DBAL\Event\Listeners\SQLSessionInit; use OC\SystemConfig; /** @@ -127,11 +126,8 @@ public function getConnection($type, $additionalConnectionParams) { $normalizedType = $this->normalizeType($type); $eventManager = new EventManager(); $eventManager->addEventSubscriber(new SetTransactionIsolationLevel()); + $additionalConnectionParams = array_merge($this->createConnectionParams(), $additionalConnectionParams); switch ($normalizedType) { - case 'mysql': - $eventManager->addEventSubscriber( - new SQLSessionInit("SET SESSION AUTOCOMMIT=1")); - break; case 'oci': $eventManager->addEventSubscriber(new OracleSessionInit); // the driverOptions are unused in dbal and need to be mapped to the parameters @@ -159,7 +155,7 @@ public function getConnection($type, $additionalConnectionParams) { } /** @var Connection $connection */ $connection = DriverManager::getConnection( - array_merge($this->getDefaultConnectionParams($type), $additionalConnectionParams), + $additionalConnectionParams, new Configuration(), $eventManager ); @@ -195,10 +191,10 @@ public function isValidType($type) { public function createConnectionParams(string $configPrefix = '') { $type = $this->config->getValue('dbtype', 'sqlite'); - $connectionParams = [ + $connectionParams = array_merge($this->getDefaultConnectionParams($type), [ 'user' => $this->config->getValue($configPrefix . 'dbuser', $this->config->getValue('dbuser', '')), 'password' => $this->config->getValue($configPrefix . 'dbpassword', $this->config->getValue('dbpassword', '')), - ]; + ]); $name = $this->config->getValue($configPrefix . 'dbname', $this->config->getValue('dbname', self::DEFAULT_DBNAME)); if ($this->normalizeType($type) === 'sqlite3') { @@ -237,7 +233,11 @@ public function createConnectionParams(string $configPrefix = '') { $connectionParams['persistent'] = true; } - return $connectionParams; + $replica = $this->config->getValue('dbreplica', []) ?: [$connectionParams]; + return array_merge($connectionParams, [ + 'primary' => $connectionParams, + 'replica' => $replica, + ]); } /** diff --git a/lib/private/DB/SetTransactionIsolationLevel.php b/lib/private/DB/SetTransactionIsolationLevel.php index b067edde441db..9d9323664c8d8 100644 --- a/lib/private/DB/SetTransactionIsolationLevel.php +++ b/lib/private/DB/SetTransactionIsolationLevel.php @@ -26,8 +26,10 @@ namespace OC\DB; use Doctrine\Common\EventSubscriber; +use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; use Doctrine\DBAL\Event\ConnectionEventArgs; use Doctrine\DBAL\Events; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\TransactionIsolationLevel; class SetTransactionIsolationLevel implements EventSubscriber { @@ -36,7 +38,13 @@ class SetTransactionIsolationLevel implements EventSubscriber { * @return void */ public function postConnect(ConnectionEventArgs $args) { - $args->getConnection()->setTransactionIsolation(TransactionIsolationLevel::READ_COMMITTED); + $connection = $args->getConnection(); + if ($connection instanceof PrimaryReadReplicaConnection && $connection->isConnectedToPrimary()) { + $connection->setTransactionIsolation(TransactionIsolationLevel::READ_COMMITTED); + if ($connection->getDatabasePlatform() instanceof MySQLPlatform) { + $connection->executeStatement('SET SESSION AUTOCOMMIT=1'); + } + } } public function getSubscribedEvents() { diff --git a/lib/private/Server.php b/lib/private/Server.php index cf4262e2d5043..a5e92bde52991 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -842,8 +842,7 @@ public function __construct($webRoot, \OC\Config $config) { if (!$factory->isValidType($type)) { throw new \OC\DatabaseException('Invalid database type'); } - $connectionParams = $factory->createConnectionParams(); - $connection = $factory->getConnection($type, $connectionParams); + $connection = $factory->getConnection($type, []); return $connection; }); /** @deprecated 19.0.0 */ diff --git a/lib/private/Setup/AbstractDatabase.php b/lib/private/Setup/AbstractDatabase.php index 6bef40338c996..88a31eaccdcfa 100644 --- a/lib/private/Setup/AbstractDatabase.php +++ b/lib/private/Setup/AbstractDatabase.php @@ -141,7 +141,7 @@ protected function connect(array $configOverwrite = []): Connection { $connectionParams['host'] = $host; } - $connectionParams = array_merge($connectionParams, $configOverwrite); + $connectionParams = array_merge($connectionParams, ['primary' => $connectionParams, 'replica' => [$connectionParams]], $configOverwrite); $cf = new ConnectionFactory($this->config); return $cf->getConnection($this->config->getValue('dbtype', 'sqlite'), $connectionParams); }