Skip to content

Commit 2d2ef65

Browse files
committed
feat: First attempt to track dirty tables after writes and switch back to replicas if reads go to other tables
Signed-off-by: Julius Härtl <[email protected]>
1 parent 2f461f5 commit 2d2ef65

1 file changed

Lines changed: 27 additions & 1 deletion

File tree

lib/private/DB/Connection.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
use OCP\PreConditionNotMetException;
5656
use OCP\Profiler\IProfiler;
5757
use Psr\Log\LoggerInterface;
58-
use SensitiveParameter;
5958

6059
class Connection extends PrimaryReadReplicaConnection {
6160
/** @var string */
@@ -80,6 +79,8 @@ class Connection extends PrimaryReadReplicaConnection {
8079
/** @var DbDataCollector|null */
8180
protected $dbDataCollector = null;
8281

82+
protected $tableDirtyWrites = [];
83+
8384
/**
8485
* Initializes a new instance of the Connection class.
8586
*
@@ -256,13 +257,35 @@ public function prepare($sql, $limit = null, $offset = null): Statement {
256257
* @throws \Doctrine\DBAL\Exception
257258
*/
258259
public function executeQuery(string $sql, array $params = [], $types = [], QueryCacheProfile $qcp = null): Result {
260+
$tables = $this->getQueriedTables($sql);
261+
if (count(array_intersect($this->tableDirtyWrites, $tables)) === 0 && !$this->isTransactionActive()) {
262+
// No tables read that could have been written already in the same request and no transaction active
263+
// so we can switch back to the replica for reading as long as no writes happen that switch back to the primary
264+
$this->ensureConnectedToReplica();
265+
$this->logger->debug('no dirty table reads: ' . $sql, ['tables' => $this->tableDirtyWrites, 'reads' => $tables]);
266+
} else {
267+
// Read to a table that was previously written to
268+
// While this might not necessarily mean that we did a read after write it is an indication for a code path to check
269+
$this->logger->debug('dirty table reads: ' . $sql, ['tables' => $this->tableDirtyWrites, 'reads' => $tables, 'exception' => new \Exception()]);
270+
}
271+
259272
$sql = $this->replaceTablePrefix($sql);
260273
$sql = $this->adapter->fixupStatement($sql);
261274
$this->queriesExecuted++;
262275
$this->logQueryToFile($sql);
263276
return parent::executeQuery($sql, $params, $types, $qcp);
264277
}
265278

279+
/**
280+
* Helper function to get the list of tables affected by a given query
281+
* used to track dirty tables that received a write with the current request
282+
*/
283+
private function getQueriedTables(string $sql): array {
284+
$re = '/(\*PREFIX\*[A-z0-9_-]+)/mi';
285+
preg_match_all($re, $sql, $matches, PREG_SET_ORDER);
286+
return array_map([$this, 'replaceTablePrefix'], $matches[0] ?? []);
287+
}
288+
266289
/**
267290
* @throws Exception
268291
*/
@@ -289,6 +312,9 @@ public function executeUpdate(string $sql, array $params = [], array $types = []
289312
* @throws \Doctrine\DBAL\Exception
290313
*/
291314
public function executeStatement($sql, array $params = [], array $types = []): int {
315+
$tables = $this->getQueriedTables($sql);
316+
$this->tableDirtyWrites = array_merge($this->tableDirtyWrites, $tables);
317+
$this->logger->error('dirty table writes: ' . $sql, ['tables' => $this->tableDirtyWrites]);
292318
$sql = $this->replaceTablePrefix($sql);
293319
$sql = $this->adapter->fixupStatement($sql);
294320
$this->queriesExecuted++;

0 commit comments

Comments
 (0)