Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 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_replica_moodle_database_table_names.php');
31
require_once(__DIR__.'/fixtures/read_replica_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_replica_trait
42
 */
43
final class dml_read_replica_test extends \database_driver_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_replica_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_replica_moodle_database::class
64
    ): read_replica_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_replica_moodle_database methods
82
     * is a readonly replica 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_replica_trait::table_names() test data provider
93
     *
94
     * @return array
95
     * @dataProvider table_names_provider
96
     */
97
    public static 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_replica_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_replica_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_replica());
159
        $this->assertNull($DB->get_dbhwrite());
160
 
161
        $handle = $DB->get_records('table');
162
        $this->assert_readonly_handle($handle);
163
        $readsreplica = $DB->perf_get_reads_replica();
164
        $this->assertGreaterThan(0, $readsreplica);
165
        $this->assertNull($DB->get_dbhwrite());
166
 
167
        $handle = $DB->get_records('table2');
168
        $this->assert_readonly_handle($handle);
169
        $readsreplica = $DB->perf_get_reads_replica();
170
        $this->assertGreaterThan(1, $readsreplica);
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($readsreplica, $DB->perf_get_reads_replica());
181
 
182
            sleep(1);
183
        }
184
 
185
        $handle = $DB->get_records('table');
186
        $this->assert_readonly_handle($handle);
187
        $this->assertEquals($readsreplica + 1, $DB->perf_get_reads_replica());
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_replica());
199
        $this->assertNull($DB->get_dbhwrite());
200
 
201
        $handle = $DB->get_records('table');
202
        $this->assert_readonly_handle($handle);
203
        $readsreplica = $DB->perf_get_reads_replica();
204
        $this->assertGreaterThan(0, $readsreplica);
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($readsreplica, $DB->perf_get_reads_replica());
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_replica());
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_replica());
229
 
230
        $handle = $DB->get_records('table');
231
        $this->assertEquals('test_rw::test:test', $handle);
232
        $this->assertEquals(0, $DB->perf_get_reads_replica());
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_replica());
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_replica());
243
 
244
        $handle = $DB->get_records('table2');
245
        $this->assert_readonly_handle($handle);
246
        $this->assertEquals(2, $DB->perf_get_reads_replica());
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_replica());
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_replica());
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_replica());
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_replica());
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_replica());
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
     */
295
    public function test_transaction(): void {
296
        $DB = $this->new_db(true);
297
 
298
        $this->assertNull($DB->get_dbhwrite());
299
 
300
        $skip = false;
301
        $transaction = $DB->start_delegated_transaction();
302
        $now = microtime(true);
303
        $handle = $DB->get_records_sql("SELECT * FROM {table}");
304
        // Use rw handle during transaction.
305
        $this->assertEquals('test_rw::test:test', $handle);
306
 
307
        $handle = $DB->insert_record_raw('table', array('name' => 'blah'));
308
        // Introduce delay so we can check that table write timestamps
309
        // are adjusted properly.
310
        sleep(1);
311
        $transaction->allow_commit();
312
        // This condition should always evaluate true, however we need to
313
        // safeguard from an unaccounted delay that can break this test.
314
        if (microtime(true) - $now < 1 + self::$dbreadonlylatency) {
315
            // Not enough time passed, use rw handle.
316
            $handle = $DB->get_records_sql("SELECT * FROM {table}");
317
            $this->assertEquals('test_rw::test:test', $handle);
318
 
319
            // Make sure enough time passes.
320
            sleep(1);
321
        } else {
322
            $skip = true;
323
        }
324
 
325
        // Exceeded latency time, use ro handle.
326
        $handle = $DB->get_records_sql("SELECT * FROM {table}");
327
        $this->assert_readonly_handle($handle);
328
 
329
        if ($skip) {
330
            $this->markTestSkipped("Delay too long to test write handle immediately after transaction");
331
        }
332
    }
333
 
334
    /**
335
     * Test readonly handle is not used immediately after update
336
     * Test last written time is adjusted post-write,
337
     * so the latency parameter is applied properly.
338
     *
339
     * @return void
340
     */
341
    public function test_long_update(): void {
342
        $DB = $this->new_db(true);
343
 
344
        $this->assertNull($DB->get_dbhwrite());
345
 
346
        $skip = false;
347
 
348
        list($sql, $params, $ptype) = $DB->fix_sql_params("UPDATE {table} SET a = 1 WHERE id = 1");
349
        $DB->with_query_start_end($sql, $params, SQL_QUERY_UPDATE, function ($dbh) use (&$now) {
350
            sleep(1);
351
            $now = microtime(true);
352
        });
353
 
354
        // This condition should always evaluate true, however we need to
355
        // safeguard from an unaccounted delay that can break this test.
356
        if (microtime(true) - $now < self::$dbreadonlylatency) {
357
            // Not enough time passed, use rw handle.
358
            $handle = $DB->get_records_sql("SELECT * FROM {table}");
359
            $this->assertEquals('test_rw::test:test', $handle);
360
 
361
            // Make sure enough time passes.
362
            sleep(1);
363
        } else {
364
            $skip = true;
365
        }
366
 
367
        // Exceeded latency time, use ro handle.
368
        $handle = $DB->get_records_sql("SELECT * FROM {table}");
369
        $this->assert_readonly_handle($handle);
370
 
371
        if ($skip) {
372
            $this->markTestSkipped("Delay too long to test write handle immediately after transaction");
373
        }
374
    }
375
 
376
    /**
377
     * Test readonly handle is not used with events
378
     * when the latency parameter is applied properly.
379
     *
380
     * @return void
381
     */
382
    public function test_transaction_with_events(): void {
383
        $this->with_global_db(function () {
384
            global $DB;
385
 
386
            $DB = $this->new_db(true, ['test_ro'], read_replica_moodle_database_special::class);
387
            $DB->set_tables([
388
                'config_plugins' => [
389
                    'columns' => [
390
                        'plugin' => (object)['meta_type' => ''],
391
                    ]
392
                ]
393
            ]);
394
 
395
            $this->assertNull($DB->get_dbhwrite());
396
 
397
            $called = false;
398
            $transaction = $DB->start_delegated_transaction();
399
            $now = microtime(true);
400
 
401
            $observers = [
402
                [
403
                    'eventname'   => '\core_tests\event\unittest_executed',
404
                    'callback'    => function (\core_tests\event\unittest_executed $event) use ($DB, $now, &$called) {
405
                        $called = true;
406
                        $this->assertFalse($DB->is_transaction_started());
407
 
408
                        // This condition should always evaluate true, however we need to
409
                        // safeguard from an unaccounted delay that can break this test.
410
                        if (microtime(true) - $now < 1 + self::$dbreadonlylatency) {
411
                            // Not enough time passed, use rw handle.
412
                            $handle = $DB->get_records_sql_p("SELECT * FROM {table}");
413
                            $this->assertEquals('test_rw::test:test', $handle);
414
 
415
                            // Make sure enough time passes.
416
                            sleep(1);
417
                        } else {
418
                            $this->markTestSkipped("Delay too long to test write handle immediately after transaction");
419
                        }
420
 
421
                        // Exceeded latency time, use ro handle.
422
                        $handle = $DB->get_records_sql_p("SELECT * FROM {table}");
423
                        $this->assertEquals('test_ro::test:test', $handle);
424
                    },
425
                    'internal'    => 0,
426
                ],
427
            ];
428
            \core\event\manager::phpunit_replace_observers($observers);
429
 
430
            $handle = $DB->get_records_sql_p("SELECT * FROM {table}");
431
            // Use rw handle during transaction.
432
            $this->assertEquals('test_rw::test:test', $handle);
433
 
434
            $handle = $DB->insert_record_raw('table', array('name' => 'blah'));
435
            // Introduce delay so we can check that table write timestamps
436
            // are adjusted properly.
437
            sleep(1);
438
            $event = \core_tests\event\unittest_executed::create([
439
                'context' => \context_system::instance(),
440
                'other' => ['sample' => 1]
441
            ]);
442
            $event->trigger();
443
            $transaction->allow_commit();
444
 
445
            $this->assertTrue($called);
446
        });
447
    }
448
 
449
    /**
450
     * Test failed readonly connection falls back to write connection.
451
     *
452
     * @return void
453
     */
454
    public function test_read_only_conn_fail(): void {
455
        $this->resetDebugging();
456
 
457
        $DB = $this->new_db(false, 'test_ro_fail');
458
 
459
        $this->assertEquals(0, $DB->perf_get_reads_replica());
460
        $this->assertNotNull($DB->get_dbhwrite());
461
 
462
        $handle = $DB->get_records('table');
463
        $this->assertEquals('test_rw::test:test', $handle);
464
        $readsreplica = $DB->perf_get_reads_replica();
465
        $this->assertEquals(0, $readsreplica);
466
 
467
        $debugging = array_map(function ($d) {
468
            return $d->message;
469
        }, $this->getDebuggingMessages());
470
        $this->resetDebugging();
471
        $this->assertEquals([
472
            'Readonly db connection failed for host test_ro_fail: test_ro_fail',
473
            'Readwrite db connection succeeded for host test_rw',
474
        ], $debugging);
475
    }
476
 
477
    /**
478
     * In multiple replicas scenario, test failed readonly connection falls back to
479
     * another readonly connection.
480
     *
481
     * @return void
482
     */
483
    public function test_read_only_conn_first_fail(): void {
484
        $this->resetDebugging();
485
 
486
        $DB = $this->new_db(false, ['test_ro_fail', 'test_ro_ok']);
487
 
488
        $this->assertEquals(0, $DB->perf_get_reads_replica());
489
        $this->assertNull($DB->get_dbhwrite());
490
 
491
        $handle = $DB->get_records('table');
492
        $this->assertEquals('test_ro_ok::test:test', $handle);
493
        $readsreplica = $DB->perf_get_reads_replica();
494
        $this->assertEquals(1, $readsreplica);
495
 
496
        $debugging = array_map(function ($d) {
497
            return $d->message;
498
        }, $this->getDebuggingMessages());
499
        $this->resetDebugging();
500
        $this->assertEquals([
501
            'Readonly db connection failed for host test_ro_fail: test_ro_fail',
502
            'Readonly db connection succeeded for host test_ro_ok',
503
        ], $debugging);
504
    }
505
 
506
    /**
507
     * Helper to restore global $DB
508
     *
509
     * @param callable $test
510
     * @return void
511
     */
512
    private function with_global_db($test) {
513
        global $DB;
514
 
515
        $dbsave = $DB;
516
        try {
517
            $test();
518
        }
519
        finally {
520
            $DB = $dbsave;
521
        }
522
    }
523
 
524
    /**
525
     * Test lock_db table exclusion
526
     *
527
     * @return void
528
     */
529
    public function test_lock_db(): void {
530
        $this->with_global_db(function () {
531
            global $DB;
532
 
533
            $DB = $this->new_db(true, ['test_ro'], read_replica_moodle_database_special::class);
534
            $DB->set_tables([
535
                'lock_db' => [
536
                    'columns' => [
537
                        'resourcekey' => (object)['meta_type' => ''],
538
                        'owner' => (object)['meta_type' => ''],
539
                    ]
540
                ]
541
            ]);
542
 
543
            $this->assertEquals(0, $DB->perf_get_reads_replica());
544
            $this->assertNull($DB->get_dbhwrite());
545
 
546
            $lockfactory = new \core\lock\db_record_lock_factory('default');
547
            if (!$lockfactory->is_available()) {
548
                $this->markTestSkipped("db_record_lock_factory not available");
549
            }
550
 
551
            $lock = $lockfactory->get_lock('abc', 2);
552
            $lock->release();
553
            $this->assertEquals(0, $DB->perf_get_reads_replica());
554
            $this->assertTrue($DB->perf_get_reads() > 0);
555
        });
556
    }
557
 
558
    /**
559
     * Test sessions table exclusion
560
     *
561
     * @return void
562
     */
563
    public function test_sessions(): void {
564
        $this->with_global_db(function () {
565
            global $DB, $CFG;
566
 
567
            $CFG->dbsessions = true;
568
            $DB = $this->new_db(true, ['test_ro'], read_replica_moodle_database_special::class);
569
            $DB->set_tables([
570
                'sessions' => [
571
                    'columns' => [
572
                        'sid' => (object)['meta_type' => ''],
573
                    ]
574
                ]
575
            ]);
576
 
577
            $this->assertEquals(0, $DB->perf_get_reads_replica());
578
            $this->assertNull($DB->get_dbhwrite());
579
 
580
            $session = new \core\session\database();
581
            $session->read('dummy');
582
 
583
            $this->assertEquals(0, $DB->perf_get_reads_replica());
584
            $this->assertTrue($DB->perf_get_reads() > 0);
585
        });
586
 
587
        \core\session\manager::restart_with_write_lock(false);
588
    }
589
}