MDL-19711 dml: Enable use of readonly slave database handles

Implemented with moodle_read_slave_trait

Functionality is triggered by supplying config dboption['readonly'].
See config-dist.php for more info on supported dboptions.

pgsql and mysqli drivers are using this feature. Also added support for
connection timeout for these two drivers.
This commit is contained in:
Srdjan 2020-05-07 14:14:29 +10:00
parent d85118369d
commit 46cfde3d95
19 changed files with 2422 additions and 28 deletions

View file

@ -25,6 +25,7 @@
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_database.php');
require_once(__DIR__.'/moodle_read_slave_trait.php');
require_once(__DIR__.'/mysqli_native_moodle_recordset.php');
require_once(__DIR__.'/mysqli_native_moodle_temptables.php');
@ -36,6 +37,9 @@ require_once(__DIR__.'/mysqli_native_moodle_temptables.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mysqli_native_moodle_database extends moodle_database {
use moodle_read_slave_trait {
can_use_readonly as read_slave_can_use_readonly;
}
/** @var mysqli $mysqli */
protected $mysqli = null;
@ -235,6 +239,14 @@ class mysqli_native_moodle_database extends moodle_database {
if (isset($this->dboptions['dbcollation'])) {
return $this->dboptions['dbcollation'];
}
}
/**
* Set 'dbcollation' option
*
* @return string $dbcollation
*/
private function detect_collation(): string {
if ($this->external) {
return null;
}
@ -246,9 +258,7 @@ class mysqli_native_moodle_database extends moodle_database {
$sql = "SELECT collation_name
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config' AND column_name = 'value'";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = $this->mysqli->query($sql);
$this->query_end($result);
if ($rec = $result->fetch_assoc()) {
// MySQL 8 BC: information_schema.* returns the fields in upper case.
$rec = array_change_key_case($rec, CASE_LOWER);
@ -260,9 +270,7 @@ class mysqli_native_moodle_database extends moodle_database {
if (!$collation) {
// Get the default database collation, but only if using UTF-8.
$sql = "SELECT @@collation_database";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = $this->mysqli->query($sql);
$this->query_end($result);
if ($rec = $result->fetch_assoc()) {
if (strpos($rec['@@collation_database'], 'utf8_') === 0 || strpos($rec['@@collation_database'], 'utf8mb4_') === 0) {
$collation = $rec['@@collation_database'];
@ -275,9 +283,7 @@ class mysqli_native_moodle_database extends moodle_database {
// We want only utf8 compatible collations.
$collation = null;
$sql = "SHOW COLLATION WHERE Collation LIKE 'utf8mb4\_%' AND Charset = 'utf8mb4'";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = $this->mysqli->query($sql);
$this->query_end($result);
while ($res = $result->fetch_assoc()) {
$collation = $res['Collation'];
if (strtoupper($res['Default']) === 'YES') {
@ -518,7 +524,6 @@ class mysqli_native_moodle_database extends moodle_database {
/**
* Connect to db
* Must be called before other methods.
* @param string $dbhost The database host.
* @param string $dbuser The database username.
* @param string $dbpass The database username's password.
@ -527,7 +532,7 @@ class mysqli_native_moodle_database extends moodle_database {
* @param array $dboptions driver specific options
* @return bool success
*/
public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
public function raw_connect(string $dbhost, string $dbuser, string $dbpass, string $dbname, $prefix, array $dboptions=null): bool {
$driverstatus = $this->driver_installed();
if ($driverstatus !== true) {
@ -556,10 +561,19 @@ class mysqli_native_moodle_database extends moodle_database {
if ($dbhost and !empty($this->dboptions['dbpersist'])) {
$dbhost = "p:$dbhost";
}
$this->mysqli = @new mysqli($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket);
$this->mysqli = mysqli_init();
if (!empty($this->dboptions['connecttimeout'])) {
$this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, $this->dboptions['connecttimeout']);
}
if ($this->mysqli->connect_errno !== 0) {
$dberr = $this->mysqli->connect_error;
$conn = null;
try {
$conn = $this->mysqli->real_connect($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket);
} catch (\Exception $e) {
$dberr = "$e";
}
if (!$conn) {
$dberr = $dberr ?: $this->mysqli->connect_error;
$this->mysqli = null;
throw new dml_connection_exception($dberr);
}
@ -568,16 +582,14 @@ class mysqli_native_moodle_database extends moodle_database {
$this->query_log_prevent();
if (isset($dboptions['dbcollation'])) {
$collationinfo = explode('_', $dboptions['dbcollation']);
$this->dboptions['dbcollation'] = $dboptions['dbcollation'];
$collation = $this->dboptions['dbcollation'] = $dboptions['dbcollation'];
} else {
$collationinfo = explode('_', $this->get_dbcollation());
$collation = $this->detect_collation();
}
$collationinfo = explode('_', $collation);
$charset = reset($collationinfo);
$this->query_start("--set_charset()", null, SQL_QUERY_AUX);
$this->mysqli->set_charset($charset);
$this->query_end(true);
// If available, enforce strict mode for the session. That guaranties
// standard behaviour under some situations, avoiding some MySQL nasty
@ -587,9 +599,7 @@ class mysqli_native_moodle_database extends moodle_database {
$si = $this->get_server_info();
if (version_compare($si['version'], '5.0.2', '>=')) {
$sql = "SET SESSION sql_mode = 'STRICT_ALL_TABLES'";
$this->query_start($sql, null, SQL_QUERY_AUX);
$result = $this->mysqli->query($sql);
$this->query_end($result);
}
// We can enable logging now.
@ -614,6 +624,40 @@ class mysqli_native_moodle_database extends moodle_database {
}
}
/**
* Gets db handle currently used with queries
* @return resource
*/
protected function get_db_handle() {
return $this->mysqli;
}
/**
* Sets db handle to be used with subsequent queries
* @param resource $dbh
* @return void
*/
protected function set_db_handle($dbh): void {
$this->mysqli = $dbh;
}
/**
* Check if The query qualifies for readonly connection execution
* Logging queries are exempt, those are write operations that circumvent
* standard query_start/query_end paths.
* @param int $type type of query
* @param string $sql
* @return bool
*/
protected function can_use_readonly(int $type, string $sql): bool {
// ... *_LOCK queries always go to master.
if (preg_match('/\b(GET|RELEASE)_LOCK/i', $sql)) {
return false;
}
return $this->read_slave_can_use_readonly($type, $sql);
}
/**
* Returns database server info array
* @return array Array containing 'description' and 'version' info