mirror of
https://github.com/moodle/moodle.git
synced 2025-08-09 10:56:56 +02:00

In practice it is as if the feature was turned off most of the time. However, some long processes may benefit from this very safe value in case admins missed to configure it.
531 lines
18 KiB
PHP
531 lines
18 KiB
PHP
<?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
|
|
* @coversDefaultClass \moodle_temptables
|
|
*/
|
|
|
|
namespace core;
|
|
|
|
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');
|
|
require_once(__DIR__.'/../../tests/fixtures/event_fixtures.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
|
|
*/
|
|
class dml_read_slave_test extends \base_testcase {
|
|
|
|
/** @var float */
|
|
static private $dbreadonlylatency = 0.8;
|
|
/** @var float */
|
|
static private $defaultlatency = 1;
|
|
|
|
/**
|
|
* 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->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());
|
|
|
|
$handle = $DB->get_records('table');
|
|
$this->assertEquals('test_rw::test:test', $handle);
|
|
$this->assertEquals(0, $DB->perf_get_reads_slave());
|
|
|
|
$handle = $DB->get_records_sql("SELECT * FROM {table2} JOIN {table}");
|
|
$this->assertEquals('test_rw::test:test', $handle);
|
|
$this->assertEquals(0, $DB->perf_get_reads_slave());
|
|
|
|
sleep(1);
|
|
|
|
$handle = $DB->get_records('table');
|
|
$this->assert_readonly_handle($handle);
|
|
$this->assertEquals(1, $DB->perf_get_reads_slave());
|
|
|
|
$handle = $DB->get_records('table2');
|
|
$this->assert_readonly_handle($handle);
|
|
$this->assertEquals(2, $DB->perf_get_reads_slave());
|
|
|
|
$handle = $DB->get_records_sql("SELECT * FROM {table2} JOIN {table}");
|
|
$this->assert_readonly_handle($handle);
|
|
$this->assertEquals(3, $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
|
|
* @covers ::can_use_readonly
|
|
* @covers ::commit_delegated_transaction
|
|
*/
|
|
public function test_transaction(): void {
|
|
$DB = $this->new_db(true);
|
|
|
|
$this->assertNull($DB->get_dbhwrite());
|
|
|
|
$skip = false;
|
|
$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);
|
|
} else {
|
|
$skip = true;
|
|
}
|
|
|
|
// Exceeded latency time, use ro handle.
|
|
$handle = $DB->get_records_sql("SELECT * FROM {table}");
|
|
$this->assert_readonly_handle($handle);
|
|
|
|
if ($skip) {
|
|
$this->markTestSkipped("Delay too long to test write handle immediately after transaction");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test readonly handle is not used with events
|
|
* when the latency parameter is applied properly.
|
|
*
|
|
* @return void
|
|
* @covers ::can_use_readonly
|
|
* @covers ::commit_delegated_transaction
|
|
*/
|
|
public function test_transaction_with_events(): void {
|
|
$this->with_global_db(function () {
|
|
global $DB;
|
|
|
|
$DB = $this->new_db(true, ['test_ro'], read_slave_moodle_database_special::class);
|
|
$DB->set_tables([
|
|
'config_plugins' => [
|
|
'columns' => [
|
|
'plugin' => (object)['meta_type' => ''],
|
|
]
|
|
]
|
|
]);
|
|
|
|
$this->assertNull($DB->get_dbhwrite());
|
|
|
|
$this->_called = false;
|
|
$transaction = $DB->start_delegated_transaction();
|
|
$now = microtime(true);
|
|
|
|
$observers = [
|
|
[
|
|
'eventname' => '\core_tests\event\unittest_executed',
|
|
'callback' => function (\core_tests\event\unittest_executed $event) use ($DB, $now) {
|
|
$this->_called = true;
|
|
$this->assertFalse($DB->is_transaction_started());
|
|
|
|
// 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_p("SELECT * FROM {table}");
|
|
$this->assertEquals('test_rw::test:test', $handle);
|
|
|
|
// Make sure enough time passes.
|
|
sleep(1);
|
|
} else {
|
|
$this->markTestSkipped("Delay too long to test write handle immediately after transaction");
|
|
}
|
|
|
|
// Exceeded latency time, use ro handle.
|
|
$handle = $DB->get_records_sql_p("SELECT * FROM {table}");
|
|
$this->assertEquals('test_ro::test:test', $handle);
|
|
},
|
|
'internal' => 0,
|
|
],
|
|
];
|
|
\core\event\manager::phpunit_replace_observers($observers);
|
|
|
|
$handle = $DB->get_records_sql_p("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);
|
|
$event = \core_tests\event\unittest_executed::create([
|
|
'context' => \context_system::instance(),
|
|
'other' => ['sample' => 1]
|
|
]);
|
|
$event->trigger();
|
|
$transaction->allow_commit();
|
|
|
|
$this->assertTrue($this->_called);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Test failed readonly connection falls back to write connection.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function test_read_only_conn_fail() : void {
|
|
$DB = $this->new_db(false, 'test_ro_fail');
|
|
|
|
$this->assertEquals(0, $DB->perf_get_reads_slave());
|
|
$this->assertNotNull($DB->get_dbhwrite());
|
|
|
|
$handle = $DB->get_records('table');
|
|
$this->assertEquals('test_rw::test:test', $handle);
|
|
$readsslave = $DB->perf_get_reads_slave();
|
|
$this->assertEquals(0, $readsslave);
|
|
}
|
|
|
|
/**
|
|
* In multiple slaves scenario, test failed readonly connection falls back to
|
|
* another readonly connection.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function test_read_only_conn_first_fail() : void {
|
|
$DB = $this->new_db(false, ['test_ro_fail', 'test_ro_ok']);
|
|
|
|
$this->assertEquals(0, $DB->perf_get_reads_slave());
|
|
$this->assertNull($DB->get_dbhwrite());
|
|
|
|
$handle = $DB->get_records('table');
|
|
$this->assertEquals('test_ro_ok::test:test', $handle);
|
|
$readsslave = $DB->perf_get_reads_slave();
|
|
$this->assertEquals(1, $readsslave);
|
|
}
|
|
|
|
/**
|
|
* Helper to restore global $DB
|
|
*
|
|
* @param callable $test
|
|
* @return void
|
|
*/
|
|
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(false);
|
|
}
|
|
}
|