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

This has been generated running the following Sniffs, all them part of the Moodle's CodeSniffer standard: - PSR12.Functions.ReturnTypeDeclaration - PSR12.Functions.NullableTypeDeclaration - moodle.Methods.MethodDeclarationSpacing - Squiz.Whitespace.ScopeKeywordSpacing All them are, exclusively, about correct spacing, so the changes are, all them, only white space changes. Only exceptions to the above are 3 changes what were setting the return type in a new line, and, when that happens, the closing parenthesis (bracket) has to go to the same line than the colon.
573 lines
20 KiB
PHP
573 lines
20 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
|
|
*/
|
|
|
|
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
|
|
* @covers \moodle_read_slave_trait
|
|
*/
|
|
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->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 immediately after update
|
|
* Test last written time is adjusted post-write,
|
|
* so the latency parameter is applied properly.
|
|
*
|
|
* @return void
|
|
* @covers ::can_use_readonly
|
|
* @covers ::query_end
|
|
*/
|
|
public function test_long_update(): void {
|
|
$DB = $this->new_db(true);
|
|
|
|
$this->assertNull($DB->get_dbhwrite());
|
|
|
|
$skip = false;
|
|
|
|
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);
|
|
$now = microtime(true);
|
|
});
|
|
|
|
// This condition should always evaluate true, however we need to
|
|
// safeguard from an unaccounted delay that can break this test.
|
|
if (microtime(true) - $now < 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());
|
|
|
|
$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, &$called) {
|
|
$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($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->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);
|
|
}
|
|
}
|