Signed-off-by: Julius Härtl <jus@bitgrid.net>tags/v29.0.0beta1
@@ -151,6 +151,14 @@ $CONFIG = [ | |||
*/ | |||
'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 |
@@ -38,6 +38,7 @@ namespace OC\DB; | |||
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\PreConditionNotMetException; | |||
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 @@ class Connection extends \Doctrine\DBAL\Connection { | |||
/** | |||
* @throws Exception | |||
*/ | |||
public function connect() { | |||
public function connect($connectionName = null) { | |||
try { | |||
if ($this->_conn) { | |||
/** @psalm-suppress InternalMethod */ | |||
@@ -302,6 +303,10 @@ class Connection extends \Doctrine\DBAL\Connection { | |||
$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 @@ class Connection extends \Doctrine\DBAL\Connection { | |||
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; | |||
} | |||
} |
@@ -32,7 +32,6 @@ use Doctrine\Common\EventManager; | |||
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 @@ class ConnectionFactory { | |||
$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 @@ class ConnectionFactory { | |||
} | |||
/** @var Connection $connection */ | |||
$connection = DriverManager::getConnection( | |||
array_merge($this->getDefaultConnectionParams($type), $additionalConnectionParams), | |||
$additionalConnectionParams, | |||
new Configuration(), | |||
$eventManager | |||
); | |||
@@ -195,10 +191,10 @@ class ConnectionFactory { | |||
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 @@ class ConnectionFactory { | |||
$connectionParams['persistent'] = true; | |||
} | |||
return $connectionParams; | |||
$replica = $this->config->getValue('dbreplica', []) ?: [$connectionParams]; | |||
return array_merge($connectionParams, [ | |||
'primary' => $connectionParams, | |||
'replica' => $replica, | |||
]); | |||
} | |||
/** |
@@ -26,8 +26,10 @@ declare(strict_types=1); | |||
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() { |
@@ -842,8 +842,7 @@ class Server extends ServerContainer implements IServerContainer { | |||
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 */ |
@@ -141,7 +141,7 @@ abstract class AbstractDatabase { | |||
$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); | |||
} |