diff options
author | Julius Härtl <jus@bitgrid.net> | 2024-01-12 12:11:43 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-12 12:11:43 +0100 |
commit | 48628b90690d8204e7875d561b8115c526cc9176 (patch) | |
tree | f88d51a5ac83edd9eaa295aa72ca21c63e9636f5 | |
parent | 32a377eaba5afe85eb545df6f59a220ea7d983e6 (diff) | |
parent | c17c42a0b246aee4417bbc1cac20cc8ff1d52a18 (diff) | |
download | nextcloud-server-48628b90690d8204e7875d561b8115c526cc9176.tar.gz nextcloud-server-48628b90690d8204e7875d561b8115c526cc9176.zip |
Merge pull request #42345 from nextcloud/enh/read-replica-followup
feat: Track dirty table writes and long transactions
-rw-r--r-- | lib/private/DB/Connection.php | 61 |
1 files changed, 61 insertions, 0 deletions
diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index e3fda3e464f..a5f41cc66c9 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -79,6 +79,10 @@ class Connection extends PrimaryReadReplicaConnection { /** @var DbDataCollector|null */ protected $dbDataCollector = null; + protected ?float $transactionActiveSince = null; + + protected $tableDirtyWrites = []; + /** * Initializes a new instance of the Connection class. * @@ -255,6 +259,18 @@ class Connection extends PrimaryReadReplicaConnection { * @throws \Doctrine\DBAL\Exception */ public function executeQuery(string $sql, array $params = [], $types = [], QueryCacheProfile $qcp = null): Result { + $tables = $this->getQueriedTables($sql); + if (count(array_intersect($this->tableDirtyWrites, $tables)) === 0 && !$this->isTransactionActive()) { + // No tables read that could have been written already in the same request and no transaction active + // so we can switch back to the replica for reading as long as no writes happen that switch back to the primary + // We cannot log here as this would log too early in the server boot process + $this->ensureConnectedToReplica(); + } else { + // Read to a table that was previously written to + // While this might not necessarily mean that we did a read after write it is an indication for a code path to check + $this->logger->debug('dirty table reads: ' . $sql, ['tables' => $this->tableDirtyWrites, 'reads' => $tables, 'exception' => new \Exception()]); + } + $sql = $this->replaceTablePrefix($sql); $sql = $this->adapter->fixupStatement($sql); $this->queriesExecuted++; @@ -263,6 +279,16 @@ class Connection extends PrimaryReadReplicaConnection { } /** + * Helper function to get the list of tables affected by a given query + * used to track dirty tables that received a write with the current request + */ + private function getQueriedTables(string $sql): array { + $re = '/(\*PREFIX\*\w+)/mi'; + preg_match_all($re, $sql, $matches); + return array_map([$this, 'replaceTablePrefix'], $matches[0] ?? []); + } + + /** * @throws Exception */ public function executeUpdate(string $sql, array $params = [], array $types = []): int { @@ -288,6 +314,9 @@ class Connection extends PrimaryReadReplicaConnection { * @throws \Doctrine\DBAL\Exception */ public function executeStatement($sql, array $params = [], array $types = []): int { + $tables = $this->getQueriedTables($sql); + $this->tableDirtyWrites = array_unique(array_merge($this->tableDirtyWrites, $tables)); + $this->logger->debug('dirty table writes: ' . $sql, ['tables' => $this->tableDirtyWrites]); $sql = $this->replaceTablePrefix($sql); $sql = $this->adapter->fixupStatement($sql); $this->queriesExecuted++; @@ -306,6 +335,7 @@ class Connection extends PrimaryReadReplicaConnection { // FIXME: Improve to log the actual target db host $isPrimary = $this->connections['primary'] === $this->_conn; $prefix .= ' ' . ($isPrimary === true ? 'primary' : 'replica') . ' '; + $prefix .= ' ' . $this->getTransactionNestingLevel() . ' '; file_put_contents( $this->systemConfig->getValue('query_log_file', ''), @@ -618,4 +648,35 @@ class Connection extends PrimaryReadReplicaConnection { } return $result; } + + public function beginTransaction() { + if (!$this->inTransaction()) { + $this->transactionActiveSince = microtime(true); + } + return parent::beginTransaction(); + } + + public function commit() { + $result = parent::commit(); + if ($this->getTransactionNestingLevel() === 0) { + $timeTook = microtime(true) - $this->transactionActiveSince; + $this->transactionActiveSince = null; + if ($timeTook > 1) { + $this->logger->warning('Transaction took longer than 1s: ' . $timeTook, ['exception' => new \Exception('Long running transaction')]); + } + } + return $result; + } + + public function rollBack() { + $result = parent::rollBack(); + if ($this->getTransactionNestingLevel() === 0) { + $timeTook = microtime(true) - $this->transactionActiveSince; + $this->transactionActiveSince = null; + if ($timeTook > 1) { + $this->logger->warning('Transaction rollback took longer than 1s: ' . $timeTook, ['exception' => new \Exception('Long running transaction rollback')]); + } + } + return $result; + } } |