Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * DML read/read-write database handle use tests
 *
 * @package    core
 * @category   dml
 * @copyright  2018 Srdjan Janković, Catalyst IT
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

defined('MOODLE_INTERNAL') || die();

require_once(__DIR__.'/fixtures/read_slave_moodle_database_table_names.php');
require_once(__DIR__.'/fixtures/read_slave_moodle_database_special.php');

/**
 * DML read/read-write database handle use tests
 *
 * @package    core
 * @category   dml
 * @copyright  2018 Catalyst IT
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
> * @covers \moodle_read_slave_trait
*/
< class core_dml_read_slave_testcase extends base_testcase {
> class dml_read_slave_test extends base_testcase {
/** @var float */ static private $dbreadonlylatency = 0.8; /** * Instantiates a test database interface object. * * @param bool $wantlatency * @param mixed $readonly * @param mixed $dbclass * @return read_slave_moodle_database $db */ public function new_db( $wantlatency = false, $readonly = [ ['dbhost' => 'test_ro1', 'dbport' => 1, 'dbuser' => 'test1', 'dbpass' => 'test1'], ['dbhost' => 'test_ro2', 'dbport' => 2, 'dbuser' => 'test2', 'dbpass' => 'test2'], ['dbhost' => 'test_ro3', 'dbport' => 3, 'dbuser' => 'test3', 'dbpass' => 'test3'], ], $dbclass = read_slave_moodle_database::class ) : read_slave_moodle_database { $dbhost = 'test_rw'; $dbname = 'test'; $dbuser = 'test'; $dbpass = 'test'; $prefix = 'test_'; $dboptions = ['readonly' => ['instance' => $readonly, 'exclude_tables' => ['exclude']]]; if ($wantlatency) { $dboptions['readonly']['latency'] = self::$dbreadonlylatency; } $db = new $dbclass(); $db->connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions); return $db; } /** * Asert that the mock handle returned from read_slave_moodle_database methods * is a readonly slave handle. * * @param string $handle * @return void */ private function assert_readonly_handle($handle) : void {
< $this->assertRegExp('/^test_ro\d:\d:test\d:test\d$/', $handle);
> $this->assertMatchesRegularExpression('/^test_ro\d:\d:test\d:test\d$/', $handle);
} /** * moodle_read_slave_trait::table_names() test data provider * * @return array * @dataProvider table_names_provider */ public function table_names_provider() : array { return [ [ "SELECT * FROM {user} u JOIN ( SELECT DISTINCT u.id FROM {user} u JOIN {user_enrolments} ue1 ON ue1.userid = u.id JOIN {enrol} e ON e.id = ue1.enrolid WHERE u.id NOT IN ( SELECT DISTINCT ue.userid FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = 1) WHERE ue.status = 'active' AND e.status = 'enabled' AND ue.timestart < now() AND (ue.timeend = 0 OR ue.timeend > now()) ) ) je ON je.id = u.id JOIN ( SELECT DISTINCT ra.userid FROM {role_assignments} ra WHERE ra.roleid IN (1, 2, 3) AND ra.contextid = 'ctx' ) rainner ON rainner.userid = u.id WHERE u.deleted = 0", [ 'user', 'user', 'user_enrolments', 'enrol', 'user_enrolments', 'enrol', 'role_assignments', ] ], ]; } /** * Test moodle_read_slave_trait::table_names() query parser. * * @param string $sql * @param array $tables * @return void * @dataProvider table_names_provider */ public function test_table_names($sql, $tables) : void { $db = new read_slave_moodle_database_table_names(); $this->assertEquals($tables, $db->table_names($db->fix_sql_params($sql)[0])); } /** * Test correct database handles are used in a read-read-write-read scenario. * Test lazy creation of the write handle. * * @return void */ public function test_read_read_write_read() : void { $DB = $this->new_db(true); $this->assertEquals(0, $DB->perf_get_reads_slave()); $this->assertNull($DB->get_dbhwrite()); $handle = $DB->get_records('table'); $this->assert_readonly_handle($handle); $readsslave = $DB->perf_get_reads_slave(); $this->assertGreaterThan(0, $readsslave); $this->assertNull($DB->get_dbhwrite()); $handle = $DB->get_records('table2'); $this->assert_readonly_handle($handle); $readsslave = $DB->perf_get_reads_slave(); $this->assertGreaterThan(1, $readsslave); $this->assertNull($DB->get_dbhwrite()); $now = microtime(true); $handle = $DB->insert_record_raw('table', array('name' => 'blah')); $this->assertEquals('test_rw::test:test', $handle); if (microtime(true) - $now < self::$dbreadonlylatency) { $handle = $DB->get_records('table'); $this->assertEquals('test_rw::test:test', $handle); $this->assertEquals($readsslave, $DB->perf_get_reads_slave()); sleep(1); } $handle = $DB->get_records('table'); $this->assert_readonly_handle($handle); $this->assertEquals($readsslave + 1, $DB->perf_get_reads_slave()); } /** * Test correct database handles are used in a read-write-write scenario. * * @return void */ public function test_read_write_write() : void { $DB = $this->new_db(); $this->assertEquals(0, $DB->perf_get_reads_slave()); $this->assertNull($DB->get_dbhwrite()); $handle = $DB->get_records('table'); $this->assert_readonly_handle($handle); $readsslave = $DB->perf_get_reads_slave(); $this->assertGreaterThan(0, $readsslave); $this->assertNull($DB->get_dbhwrite()); $handle = $DB->insert_record_raw('table', array('name' => 'blah')); $this->assertEquals('test_rw::test:test', $handle); $handle = $DB->update_record_raw('table', array('id' => 1, 'name' => 'blah2')); $this->assertEquals('test_rw::test:test', $handle); $this->assertEquals($readsslave, $DB->perf_get_reads_slave()); } /** * Test correct database handles are used in a write-read-read scenario. * * @return void */ public function test_write_read_read() : void { $DB = $this->new_db(); $this->assertEquals(0, $DB->perf_get_reads_slave()); $this->assertNull($DB->get_dbhwrite()); $handle = $DB->insert_record_raw('table', array('name' => 'blah')); $this->assertEquals('test_rw::test:test', $handle); $this->assertEquals(0, $DB->perf_get_reads_slave()); sleep(1); $handle = $DB->get_records('table'); $this->assertEquals('test_rw::test:test', $handle); $this->assertEquals(0, $DB->perf_get_reads_slave()); $handle = $DB->get_records('table2'); $this->assert_readonly_handle($handle); $this->assertEquals(1, $DB->perf_get_reads_slave()); $handle = $DB->get_records_sql("SELECT * FROM {table2} JOIN {table}"); $this->assertEquals('test_rw::test:test', $handle); $this->assertEquals(1, $DB->perf_get_reads_slave()); } /** * Test readonly handle is not used for reading from temptables. * * @return void */ public function test_read_temptable() : void { $DB = $this->new_db(); $DB->add_temptable('temptable1'); $this->assertEquals(0, $DB->perf_get_reads_slave()); $this->assertNull($DB->get_dbhwrite()); $handle = $DB->get_records('temptable1'); $this->assertEquals('test_rw::test:test', $handle); $this->assertEquals(0, $DB->perf_get_reads_slave()); $DB->delete_temptable('temptable1'); } /** * Test readonly handle is not used for reading from excluded tables. * * @return void */ public function test_read_excluded_tables() : void { $DB = $this->new_db(); $this->assertEquals(0, $DB->perf_get_reads_slave()); $this->assertNull($DB->get_dbhwrite()); $handle = $DB->get_records('exclude'); $this->assertEquals('test_rw::test:test', $handle); $this->assertEquals(0, $DB->perf_get_reads_slave()); } /** * Test readonly handle is not used during transactions. * Test last written time is adjusted post-transaction, * so the latency parameter is applied properly. * * @return void */ public function test_transaction() : void { $DB = $this->new_db(true); $this->assertNull($DB->get_dbhwrite()); $transaction = $DB->start_delegated_transaction(); $now = microtime(true); $handle = $DB->get_records_sql("SELECT * FROM {table}"); // Use rw handle during transaction. $this->assertEquals('test_rw::test:test', $handle); $handle = $DB->insert_record_raw('table', array('name' => 'blah')); // Introduce delay so we can check that table write timestamps // are adjusted properly. sleep(1); $transaction->allow_commit(); // This condition should always evaluate true, however we need to // safeguard from an unaccounted delay that can break this test. if (microtime(true) - $now < 1 + self::$dbreadonlylatency) { // Not enough time passed, use rw handle. $handle = $DB->get_records_sql("SELECT * FROM {table}"); $this->assertEquals('test_rw::test:test', $handle); // Make sure enough time passes. sleep(1); } // Exceeded latency time, use ro handle. $handle = $DB->get_records_sql("SELECT * FROM {table}"); $this->assert_readonly_handle($handle); } /**
> * Test readonly handle is not used immediately after update * Test failed readonly connection falls back to write connection. > * Test last written time is adjusted post-write, * > * so the latency parameter is applied properly. * @return void > * */ > * @return void public function test_read_only_conn_fail() : void { > * @covers ::can_use_readonly $DB = $this->new_db(false, 'test_ro_fail'); > * @covers ::query_end > */ $this->assertEquals(0, $DB->perf_get_reads_slave()); > public function test_long_update(): void { $this->assertNotNull($DB->get_dbhwrite()); > $DB = $this->new_db(true); > $handle = $DB->get_records('table'); > $this->assertNull($DB->get_dbhwrite()); $this->assertEquals('test_rw::test:test', $handle); > $readsslave = $DB->perf_get_reads_slave(); > $skip = false; $this->assertEquals(0, $readsslave); > } > list($sql, $params, $ptype) = $DB->fix_sql_params("UPDATE {table} SET a = 1 WHERE id = 1"); > $DB->with_query_start_end($sql, $params, SQL_QUERY_UPDATE, function ($dbh) use (&$now) { /** > sleep(1); * In multiple slaves scenario, test failed readonly connection falls back to > $now = microtime(true); * another readonly connection. > }); * > * @return void > // This condition should always evaluate true, however we need to */ > // safeguard from an unaccounted delay that can break this test. public function test_read_only_conn_first_fail() : void { > if (microtime(true) - $now < self::$dbreadonlylatency) { $DB = $this->new_db(false, ['test_ro_fail', 'test_ro_ok']); > // Not enough time passed, use rw handle. > $handle = $DB->get_records_sql("SELECT * FROM {table}"); $this->assertEquals(0, $DB->perf_get_reads_slave()); > $this->assertEquals('test_rw::test:test', $handle); $this->assertNull($DB->get_dbhwrite()); > > // Make sure enough time passes. $handle = $DB->get_records('table'); > sleep(1); $this->assertEquals('test_ro_ok::test:test', $handle); > } else { $readsslave = $DB->perf_get_reads_slave(); > $skip = true; $this->assertEquals(1, $readsslave); > } } > > // Exceeded latency time, use ro handle. /** > $handle = $DB->get_records_sql("SELECT * FROM {table}"); * Helper to restore global $DB > $this->assert_readonly_handle($handle); * > * @param callable $test > if ($skip) { * @return void > $this->markTestSkipped("Delay too long to test write handle immediately after transaction"); */ > } private function with_global_db($test) { > } global $DB; > > /**
$dbsave = $DB; try { $test(); } finally { $DB = $dbsave; } } /** * Test lock_db table exclusion * * @return void */ public function test_lock_db() : void { $this->with_global_db(function () { global $DB; $DB = $this->new_db(true, ['test_ro'], read_slave_moodle_database_special::class); $DB->set_tables([ 'lock_db' => [ 'columns' => [ 'resourcekey' => (object)['meta_type' => ''], 'owner' => (object)['meta_type' => ''], ] ] ]); $this->assertEquals(0, $DB->perf_get_reads_slave()); $this->assertNull($DB->get_dbhwrite()); $lockfactory = new \core\lock\db_record_lock_factory('default'); if (!$lockfactory->is_available()) { $this->markTestSkipped("db_record_lock_factory not available"); } $lock = $lockfactory->get_lock('abc', 2); $lock->release(); $this->assertEquals(0, $DB->perf_get_reads_slave()); $this->assertTrue($DB->perf_get_reads() > 0); }); } /** * Test sessions table exclusion * * @return void */ public function test_sessions() : void { $this->with_global_db(function () { global $DB, $CFG; $CFG->dbsessions = true; $DB = $this->new_db(true, ['test_ro'], read_slave_moodle_database_special::class); $DB->set_tables([ 'sessions' => [ 'columns' => [ 'sid' => (object)['meta_type' => ''], ] ] ]); $this->assertEquals(0, $DB->perf_get_reads_slave()); $this->assertNull($DB->get_dbhwrite()); $session = new \core\session\database(); $session->handler_read('dummy'); $this->assertEquals(0, $DB->perf_get_reads_slave()); $this->assertTrue($DB->perf_get_reads() > 0); });
< \core\session\manager::restart_with_write_lock();
> \core\session\manager::restart_with_write_lock(false);
} }