From 65e3016c171bdf6ab76749324d4be66797e1ca49 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 | 5 +++++ lib/private/DB/Connection.php | 20 +++++++++++++++++-- lib/private/DB/ConnectionFactory.php | 10 +++++++--- .../DB/SetTransactionIsolationLevel.php | 5 ++++- lib/private/Setup/AbstractDatabase.php | 2 +- 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/config/config.sample.php b/config/config.sample.php index a1f7332c4047b..d7f53c40d2725 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -151,6 +151,11 @@ */ 'dbpersistent' => '', +'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..80a94ea0c3b42 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; @@ -54,8 +55,9 @@ use OCP\PreConditionNotMetException; use OCP\Profiler\IProfiler; use Psr\Log\LoggerInterface; +use SensitiveParameter; -class Connection extends \Doctrine\DBAL\Connection { +class Connection extends PrimaryReadReplicaConnection { /** @var string */ protected $tablePrefix; @@ -119,7 +121,7 @@ public function __construct( /** * @throws Exception */ - public function connect() { + public function connect($connectionName = null) { try { if ($this->_conn) { /** @psalm-suppress InternalMethod */ @@ -302,6 +304,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 +609,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..c05717367aff7 100644 --- a/lib/private/DB/ConnectionFactory.php +++ b/lib/private/DB/ConnectionFactory.php @@ -129,8 +129,8 @@ public function getConnection($type, $additionalConnectionParams) { $eventManager->addEventSubscriber(new SetTransactionIsolationLevel()); switch ($normalizedType) { case 'mysql': - $eventManager->addEventSubscriber( - new SQLSessionInit("SET SESSION AUTOCOMMIT=1")); + // $eventManager->addEventSubscriber( + // new SQLSessionInit("SET SESSION AUTOCOMMIT=1")); break; case 'oci': $eventManager->addEventSubscriber(new OracleSessionInit); @@ -237,7 +237,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..83c480e8d4c8e 100644 --- a/lib/private/DB/SetTransactionIsolationLevel.php +++ b/lib/private/DB/SetTransactionIsolationLevel.php @@ -36,7 +36,10 @@ class SetTransactionIsolationLevel implements EventSubscriber { * @return void */ public function postConnect(ConnectionEventArgs $args) { - $args->getConnection()->setTransactionIsolation(TransactionIsolationLevel::READ_COMMITTED); + // Maybe this has an effect on primary + if ($args->getConnection()->isConnectedToPrimary()) { + $args->getConnection()->setTransactionIsolation(TransactionIsolationLevel::READ_COMMITTED); + } } public function getSubscribedEvents() { 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); }