Skip to content

Commit

Permalink
feat: First attempt to track dirty tables after writes and switch bac…
Browse files Browse the repository at this point in the history
…k to replicas if reads go to other tables

Signed-off-by: Julius Härtl <jus@bitgrid.net>
  • Loading branch information
juliushaertl committed Dec 15, 2023
1 parent 064003b commit 65089ee
Showing 1 changed file with 27 additions and 1 deletion.
28 changes: 27 additions & 1 deletion lib/private/DB/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
use OCP\PreConditionNotMetException;
use OCP\Profiler\IProfiler;
use Psr\Log\LoggerInterface;
use SensitiveParameter;

class Connection extends PrimaryReadReplicaConnection {
/** @var string */
Expand All @@ -80,6 +79,8 @@ class Connection extends PrimaryReadReplicaConnection {
/** @var DbDataCollector|null */
protected $dbDataCollector = null;

protected $tableDirtyWrites = [];

/**
* Initializes a new instance of the Connection class.
*
Expand Down Expand Up @@ -256,13 +257,35 @@ 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++;
$this->logQueryToFile($sql);
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, PREG_SET_ORDER);
return array_map([$this, 'replaceTablePrefix'], $matches[0] ?? []);
}

/**
* @throws Exception
*/
Expand All @@ -289,6 +312,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_merge($this->tableDirtyWrites, $tables);
$this->logger->error('dirty table writes: ' . $sql, ['tables' => $this->tableDirtyWrites]);
$sql = $this->replaceTablePrefix($sql);
$sql = $this->adapter->fixupStatement($sql);
$this->queriesExecuted++;
Expand Down

0 comments on commit 65089ee

Please sign in to comment.