From 08c3a4dc3d909a6b227e132a3af7073a910d9b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Fri, 15 Dec 2023 21:52:31 +0100 Subject: [PATCH] feat: First attempt to track dirty tables after writes and switch back to replicas if reads go to other tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/private/DB/Connection.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index d3cc93cdbe009..cae50da1455f2 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -81,6 +81,8 @@ class Connection extends PrimaryReadReplicaConnection { protected ?float $transactionActiveSince = null; + protected $tableDirtyWrites = []; + /** * Initializes a new instance of the Connection class. * @@ -257,6 +259,18 @@ public function prepare($sql, $limit = null, $offset = null): Statement { * @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 + $this->ensureConnectedToReplica(); + $this->logger->debug('no dirty table reads: ' . $sql, ['tables' => $this->tableDirtyWrites, 'reads' => $tables]); + } 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++; @@ -264,6 +278,16 @@ public function executeQuery(string $sql, array $params = [], $types = [], Query return parent::executeQuery($sql, $params, $types, $qcp); } + /** + * 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\*[A-z0-9_-]+)/mi'; + preg_match_all($re, $sql, $matches); + return array_map([$this, 'replaceTablePrefix'], $matches[0] ?? []); + } + /** * @throws Exception */ @@ -290,6 +314,9 @@ public function executeUpdate(string $sql, array $params = [], array $types = [] * @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++;