See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * DML read/read-write database handle use tests 19 * 20 * @package core 21 * @category dml 22 * @copyright 2018 Srdjan Janković, Catalyst IT 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace core; 27 28 defined('MOODLE_INTERNAL') || die(); 29 30 require_once (__DIR__.'/fixtures/read_slave_moodle_database_table_names.php'); 31 require_once (__DIR__.'/fixtures/read_slave_moodle_database_special.php'); 32 require_once (__DIR__.'/../../tests/fixtures/event_fixtures.php'); 33 34 /** 35 * DML read/read-write database handle use tests 36 * 37 * @package core 38 * @category dml 39 * @copyright 2018 Catalyst IT 40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 41 * @covers \moodle_read_slave_trait 42 */ 43 class dml_read_slave_test extends \base_testcase { 44 45 /** @var float */ 46 static private $dbreadonlylatency = 0.8; 47 48 /** 49 * Instantiates a test database interface object. 50 * 51 * @param bool $wantlatency 52 * @param mixed $readonly 53 * @param mixed $dbclass 54 * @return read_slave_moodle_database $db 55 */ 56 public function new_db( 57 $wantlatency = false, 58 $readonly = [ 59 ['dbhost' => 'test_ro1', 'dbport' => 1, 'dbuser' => 'test1', 'dbpass' => 'test1'], 60 ['dbhost' => 'test_ro2', 'dbport' => 2, 'dbuser' => 'test2', 'dbpass' => 'test2'], 61 ['dbhost' => 'test_ro3', 'dbport' => 3, 'dbuser' => 'test3', 'dbpass' => 'test3'], 62 ], 63 $dbclass = read_slave_moodle_database::class 64 ) : read_slave_moodle_database { 65 $dbhost = 'test_rw'; 66 $dbname = 'test'; 67 $dbuser = 'test'; 68 $dbpass = 'test'; 69 $prefix = 'test_'; 70 $dboptions = ['readonly' => ['instance' => $readonly, 'exclude_tables' => ['exclude']]]; 71 if ($wantlatency) { 72 $dboptions['readonly']['latency'] = self::$dbreadonlylatency; 73 } 74 75 $db = new $dbclass(); 76 $db->connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions); 77 return $db; 78 } 79 80 /** 81 * Asert that the mock handle returned from read_slave_moodle_database methods 82 * is a readonly slave handle. 83 * 84 * @param string $handle 85 * @return void 86 */ 87 private function assert_readonly_handle($handle) : void { 88 $this->assertMatchesRegularExpression('/^test_ro\d:\d:test\d:test\d$/', $handle); 89 } 90 91 /** 92 * moodle_read_slave_trait::table_names() test data provider 93 * 94 * @return array 95 * @dataProvider table_names_provider 96 */ 97 public function table_names_provider() : array { 98 return [ 99 [ 100 "SELECT * 101 FROM {user} u 102 JOIN ( 103 SELECT DISTINCT u.id FROM {user} u 104 JOIN {user_enrolments} ue1 ON ue1.userid = u.id 105 JOIN {enrol} e ON e.id = ue1.enrolid 106 WHERE u.id NOT IN ( 107 SELECT DISTINCT ue.userid FROM {user_enrolments} ue 108 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = 1) 109 WHERE ue.status = 'active' 110 AND e.status = 'enabled' 111 AND ue.timestart < now() 112 AND (ue.timeend = 0 OR ue.timeend > now()) 113 ) 114 ) je ON je.id = u.id 115 JOIN ( 116 SELECT DISTINCT ra.userid 117 FROM {role_assignments} ra 118 WHERE ra.roleid IN (1, 2, 3) 119 AND ra.contextid = 'ctx' 120 ) rainner ON rainner.userid = u.id 121 WHERE u.deleted = 0", 122 [ 123 'user', 124 'user', 125 'user_enrolments', 126 'enrol', 127 'user_enrolments', 128 'enrol', 129 'role_assignments', 130 ] 131 ], 132 ]; 133 } 134 135 /** 136 * Test moodle_read_slave_trait::table_names() query parser. 137 * 138 * @param string $sql 139 * @param array $tables 140 * @return void 141 * @dataProvider table_names_provider 142 */ 143 public function test_table_names($sql, $tables) : void { 144 $db = new read_slave_moodle_database_table_names(); 145 146 $this->assertEquals($tables, $db->table_names($db->fix_sql_params($sql)[0])); 147 } 148 149 /** 150 * Test correct database handles are used in a read-read-write-read scenario. 151 * Test lazy creation of the write handle. 152 * 153 * @return void 154 */ 155 public function test_read_read_write_read() : void { 156 $DB = $this->new_db(true); 157 158 $this->assertEquals(0, $DB->perf_get_reads_slave()); 159 $this->assertNull($DB->get_dbhwrite()); 160 161 $handle = $DB->get_records('table'); 162 $this->assert_readonly_handle($handle); 163 $readsslave = $DB->perf_get_reads_slave(); 164 $this->assertGreaterThan(0, $readsslave); 165 $this->assertNull($DB->get_dbhwrite()); 166 167 $handle = $DB->get_records('table2'); 168 $this->assert_readonly_handle($handle); 169 $readsslave = $DB->perf_get_reads_slave(); 170 $this->assertGreaterThan(1, $readsslave); 171 $this->assertNull($DB->get_dbhwrite()); 172 173 $now = microtime(true); 174 $handle = $DB->insert_record_raw('table', array('name' => 'blah')); 175 $this->assertEquals('test_rw::test:test', $handle); 176 177 if (microtime(true) - $now < self::$dbreadonlylatency) { 178 $handle = $DB->get_records('table'); 179 $this->assertEquals('test_rw::test:test', $handle); 180 $this->assertEquals($readsslave, $DB->perf_get_reads_slave()); 181 182 sleep(1); 183 } 184 185 $handle = $DB->get_records('table'); 186 $this->assert_readonly_handle($handle); 187 $this->assertEquals($readsslave + 1, $DB->perf_get_reads_slave()); 188 } 189 190 /** 191 * Test correct database handles are used in a read-write-write scenario. 192 * 193 * @return void 194 */ 195 public function test_read_write_write() : void { 196 $DB = $this->new_db(); 197 198 $this->assertEquals(0, $DB->perf_get_reads_slave()); 199 $this->assertNull($DB->get_dbhwrite()); 200 201 $handle = $DB->get_records('table'); 202 $this->assert_readonly_handle($handle); 203 $readsslave = $DB->perf_get_reads_slave(); 204 $this->assertGreaterThan(0, $readsslave); 205 $this->assertNull($DB->get_dbhwrite()); 206 207 $handle = $DB->insert_record_raw('table', array('name' => 'blah')); 208 $this->assertEquals('test_rw::test:test', $handle); 209 210 $handle = $DB->update_record_raw('table', array('id' => 1, 'name' => 'blah2')); 211 $this->assertEquals('test_rw::test:test', $handle); 212 $this->assertEquals($readsslave, $DB->perf_get_reads_slave()); 213 } 214 215 /** 216 * Test correct database handles are used in a write-read-read scenario. 217 * 218 * @return void 219 */ 220 public function test_write_read_read() : void { 221 $DB = $this->new_db(); 222 223 $this->assertEquals(0, $DB->perf_get_reads_slave()); 224 $this->assertNull($DB->get_dbhwrite()); 225 226 $handle = $DB->insert_record_raw('table', array('name' => 'blah')); 227 $this->assertEquals('test_rw::test:test', $handle); 228 $this->assertEquals(0, $DB->perf_get_reads_slave()); 229 230 $handle = $DB->get_records('table'); 231 $this->assertEquals('test_rw::test:test', $handle); 232 $this->assertEquals(0, $DB->perf_get_reads_slave()); 233 234 $handle = $DB->get_records_sql("SELECT * FROM {table2} JOIN {table}"); 235 $this->assertEquals('test_rw::test:test', $handle); 236 $this->assertEquals(0, $DB->perf_get_reads_slave()); 237 238 sleep(1); 239 240 $handle = $DB->get_records('table'); 241 $this->assert_readonly_handle($handle); 242 $this->assertEquals(1, $DB->perf_get_reads_slave()); 243 244 $handle = $DB->get_records('table2'); 245 $this->assert_readonly_handle($handle); 246 $this->assertEquals(2, $DB->perf_get_reads_slave()); 247 248 $handle = $DB->get_records_sql("SELECT * FROM {table2} JOIN {table}"); 249 $this->assert_readonly_handle($handle); 250 $this->assertEquals(3, $DB->perf_get_reads_slave()); 251 } 252 253 /** 254 * Test readonly handle is not used for reading from temptables. 255 * 256 * @return void 257 */ 258 public function test_read_temptable() : void { 259 $DB = $this->new_db(); 260 $DB->add_temptable('temptable1'); 261 262 $this->assertEquals(0, $DB->perf_get_reads_slave()); 263 $this->assertNull($DB->get_dbhwrite()); 264 265 $handle = $DB->get_records('temptable1'); 266 $this->assertEquals('test_rw::test:test', $handle); 267 $this->assertEquals(0, $DB->perf_get_reads_slave()); 268 269 $DB->delete_temptable('temptable1'); 270 } 271 272 /** 273 * Test readonly handle is not used for reading from excluded tables. 274 * 275 * @return void 276 */ 277 public function test_read_excluded_tables() : void { 278 $DB = $this->new_db(); 279 280 $this->assertEquals(0, $DB->perf_get_reads_slave()); 281 $this->assertNull($DB->get_dbhwrite()); 282 283 $handle = $DB->get_records('exclude'); 284 $this->assertEquals('test_rw::test:test', $handle); 285 $this->assertEquals(0, $DB->perf_get_reads_slave()); 286 } 287 288 /** 289 * Test readonly handle is not used during transactions. 290 * Test last written time is adjusted post-transaction, 291 * so the latency parameter is applied properly. 292 * 293 * @return void 294 * @covers ::can_use_readonly 295 * @covers ::commit_delegated_transaction 296 */ 297 public function test_transaction(): void { 298 $DB = $this->new_db(true); 299 300 $this->assertNull($DB->get_dbhwrite()); 301 302 $skip = false; 303 $transaction = $DB->start_delegated_transaction(); 304 $now = microtime(true); 305 $handle = $DB->get_records_sql("SELECT * FROM {table}"); 306 // Use rw handle during transaction. 307 $this->assertEquals('test_rw::test:test', $handle); 308 309 $handle = $DB->insert_record_raw('table', array('name' => 'blah')); 310 // Introduce delay so we can check that table write timestamps 311 // are adjusted properly. 312 sleep(1); 313 $transaction->allow_commit(); 314 // This condition should always evaluate true, however we need to 315 // safeguard from an unaccounted delay that can break this test. 316 if (microtime(true) - $now < 1 + self::$dbreadonlylatency) { 317 // Not enough time passed, use rw handle. 318 $handle = $DB->get_records_sql("SELECT * FROM {table}"); 319 $this->assertEquals('test_rw::test:test', $handle); 320 321 // Make sure enough time passes. 322 sleep(1); 323 } else { 324 $skip = true; 325 } 326 327 // Exceeded latency time, use ro handle. 328 $handle = $DB->get_records_sql("SELECT * FROM {table}"); 329 $this->assert_readonly_handle($handle); 330 331 if ($skip) { 332 $this->markTestSkipped("Delay too long to test write handle immediately after transaction"); 333 } 334 } 335 336 /** 337 * Test readonly handle is not used immediately after update 338 * Test last written time is adjusted post-write, 339 * so the latency parameter is applied properly. 340 * 341 * @return void 342 * @covers ::can_use_readonly 343 * @covers ::query_end 344 */ 345 public function test_long_update(): void { 346 $DB = $this->new_db(true); 347 348 $this->assertNull($DB->get_dbhwrite()); 349 350 $skip = false; 351 352 list($sql, $params, $ptype) = $DB->fix_sql_params("UPDATE {table} SET a = 1 WHERE id = 1"); 353 $DB->with_query_start_end($sql, $params, SQL_QUERY_UPDATE, function ($dbh) use (&$now) { 354 sleep(1); 355 $now = microtime(true); 356 }); 357 358 // This condition should always evaluate true, however we need to 359 // safeguard from an unaccounted delay that can break this test. 360 if (microtime(true) - $now < self::$dbreadonlylatency) { 361 // Not enough time passed, use rw handle. 362 $handle = $DB->get_records_sql("SELECT * FROM {table}"); 363 $this->assertEquals('test_rw::test:test', $handle); 364 365 // Make sure enough time passes. 366 sleep(1); 367 } else { 368 $skip = true; 369 } 370 371 // Exceeded latency time, use ro handle. 372 $handle = $DB->get_records_sql("SELECT * FROM {table}"); 373 $this->assert_readonly_handle($handle); 374 375 if ($skip) { 376 $this->markTestSkipped("Delay too long to test write handle immediately after transaction"); 377 } 378 } 379 380 /** 381 * Test readonly handle is not used with events 382 * when the latency parameter is applied properly. 383 * 384 * @return void 385 * @covers ::can_use_readonly 386 * @covers ::commit_delegated_transaction 387 */ 388 public function test_transaction_with_events(): void { 389 $this->with_global_db(function () { 390 global $DB; 391 392 $DB = $this->new_db(true, ['test_ro'], read_slave_moodle_database_special::class); 393 $DB->set_tables([ 394 'config_plugins' => [ 395 'columns' => [ 396 'plugin' => (object)['meta_type' => ''], 397 ] 398 ] 399 ]); 400 401 $this->assertNull($DB->get_dbhwrite()); 402 403 $this->_called = false; 404 $transaction = $DB->start_delegated_transaction(); 405 $now = microtime(true); 406 407 $observers = [ 408 [ 409 'eventname' => '\core_tests\event\unittest_executed', 410 'callback' => function (\core_tests\event\unittest_executed $event) use ($DB, $now) { 411 $this->_called = true; 412 $this->assertFalse($DB->is_transaction_started()); 413 414 // This condition should always evaluate true, however we need to 415 // safeguard from an unaccounted delay that can break this test. 416 if (microtime(true) - $now < 1 + self::$dbreadonlylatency) { 417 // Not enough time passed, use rw handle. 418 $handle = $DB->get_records_sql_p("SELECT * FROM {table}"); 419 $this->assertEquals('test_rw::test:test', $handle); 420 421 // Make sure enough time passes. 422 sleep(1); 423 } else { 424 $this->markTestSkipped("Delay too long to test write handle immediately after transaction"); 425 } 426 427 // Exceeded latency time, use ro handle. 428 $handle = $DB->get_records_sql_p("SELECT * FROM {table}"); 429 $this->assertEquals('test_ro::test:test', $handle); 430 }, 431 'internal' => 0, 432 ], 433 ]; 434 \core\event\manager::phpunit_replace_observers($observers); 435 436 $handle = $DB->get_records_sql_p("SELECT * FROM {table}"); 437 // Use rw handle during transaction. 438 $this->assertEquals('test_rw::test:test', $handle); 439 440 $handle = $DB->insert_record_raw('table', array('name' => 'blah')); 441 // Introduce delay so we can check that table write timestamps 442 // are adjusted properly. 443 sleep(1); 444 $event = \core_tests\event\unittest_executed::create([ 445 'context' => \context_system::instance(), 446 'other' => ['sample' => 1] 447 ]); 448 $event->trigger(); 449 $transaction->allow_commit(); 450 451 $this->assertTrue($this->_called); 452 }); 453 } 454 455 /** 456 * Test failed readonly connection falls back to write connection. 457 * 458 * @return void 459 */ 460 public function test_read_only_conn_fail() : void { 461 $DB = $this->new_db(false, 'test_ro_fail'); 462 463 $this->assertEquals(0, $DB->perf_get_reads_slave()); 464 $this->assertNotNull($DB->get_dbhwrite()); 465 466 $handle = $DB->get_records('table'); 467 $this->assertEquals('test_rw::test:test', $handle); 468 $readsslave = $DB->perf_get_reads_slave(); 469 $this->assertEquals(0, $readsslave); 470 } 471 472 /** 473 * In multiple slaves scenario, test failed readonly connection falls back to 474 * another readonly connection. 475 * 476 * @return void 477 */ 478 public function test_read_only_conn_first_fail() : void { 479 $DB = $this->new_db(false, ['test_ro_fail', 'test_ro_ok']); 480 481 $this->assertEquals(0, $DB->perf_get_reads_slave()); 482 $this->assertNull($DB->get_dbhwrite()); 483 484 $handle = $DB->get_records('table'); 485 $this->assertEquals('test_ro_ok::test:test', $handle); 486 $readsslave = $DB->perf_get_reads_slave(); 487 $this->assertEquals(1, $readsslave); 488 } 489 490 /** 491 * Helper to restore global $DB 492 * 493 * @param callable $test 494 * @return void 495 */ 496 private function with_global_db($test) { 497 global $DB; 498 499 $dbsave = $DB; 500 try { 501 $test(); 502 } 503 finally { 504 $DB = $dbsave; 505 } 506 } 507 508 /** 509 * Test lock_db table exclusion 510 * 511 * @return void 512 */ 513 public function test_lock_db() : void { 514 $this->with_global_db(function () { 515 global $DB; 516 517 $DB = $this->new_db(true, ['test_ro'], read_slave_moodle_database_special::class); 518 $DB->set_tables([ 519 'lock_db' => [ 520 'columns' => [ 521 'resourcekey' => (object)['meta_type' => ''], 522 'owner' => (object)['meta_type' => ''], 523 ] 524 ] 525 ]); 526 527 $this->assertEquals(0, $DB->perf_get_reads_slave()); 528 $this->assertNull($DB->get_dbhwrite()); 529 530 $lockfactory = new \core\lock\db_record_lock_factory('default'); 531 if (!$lockfactory->is_available()) { 532 $this->markTestSkipped("db_record_lock_factory not available"); 533 } 534 535 $lock = $lockfactory->get_lock('abc', 2); 536 $lock->release(); 537 $this->assertEquals(0, $DB->perf_get_reads_slave()); 538 $this->assertTrue($DB->perf_get_reads() > 0); 539 }); 540 } 541 542 /** 543 * Test sessions table exclusion 544 * 545 * @return void 546 */ 547 public function test_sessions() : void { 548 $this->with_global_db(function () { 549 global $DB, $CFG; 550 551 $CFG->dbsessions = true; 552 $DB = $this->new_db(true, ['test_ro'], read_slave_moodle_database_special::class); 553 $DB->set_tables([ 554 'sessions' => [ 555 'columns' => [ 556 'sid' => (object)['meta_type' => ''], 557 ] 558 ] 559 ]); 560 561 $this->assertEquals(0, $DB->perf_get_reads_slave()); 562 $this->assertNull($DB->get_dbhwrite()); 563 564 $session = new \core\session\database(); 565 $session->handler_read('dummy'); 566 567 $this->assertEquals(0, $DB->perf_get_reads_slave()); 568 $this->assertTrue($DB->perf_get_reads() > 0); 569 }); 570 571 \core\session\manager::restart_with_write_lock(false); 572 } 573 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body