7 use List::Util 'first';
8 use Scalar::Util 'reftype';
13 eval "use DBIx::Class::Storage::DBI::Replicated; use Test::Moose";
15 ? ( skip_all => "Deps not installed: $@" )
19 use_ok 'DBIx::Class::Storage::DBI::Replicated::Pool';
20 use_ok 'DBIx::Class::Storage::DBI::Replicated::Balancer';
21 use_ok 'DBIx::Class::Storage::DBI::Replicated::Replicant';
22 use_ok 'DBIx::Class::Storage::DBI::Replicated';
26 This is a test of the replicated storage system. This will work in one of
27 two ways, either it was try to fake replication with a couple of SQLite DBs
28 and creative use of copy, or if you define a couple of %ENV vars correctly
29 will try to test those. If you do that, it will assume the setup is properly
30 replicating. Your results may vary, but I have demonstrated this to work with
31 mysql native replication.
36 ## ----------------------------------------------------------------------------
37 ## Build a class to hold all our required testing data and methods.
38 ## ----------------------------------------------------------------------------
42 ## --------------------------------------------------------------------- ##
43 ## Create an object to contain your replicated stuff.
44 ## --------------------------------------------------------------------- ##
46 package DBIx::Class::DBI::Replicated::TestReplication;
49 use base qw/Class::Accessor::Fast/;
51 __PACKAGE__->mk_accessors( qw/schema/ );
53 ## Initialize the object
56 my ($class, $schema_method) = (shift, shift);
57 my $self = $class->SUPER::new(@_);
59 $self->schema( $self->init_schema($schema_method) );
63 ## Get the Schema and set the replication storage type
66 # current SQLT SQLite producer does not handle DROP TABLE IF EXISTS, trap warnings here
67 local $SIG{__WARN__} = sub { warn @_ unless $_[0] =~ /no such table.+DROP TABLE/ };
69 my ($class, $schema_method) = @_;
71 my $method = "get_schema_$schema_method";
72 my $schema = $class->$method;
77 sub get_schema_by_storage_type {
78 DBICTest->init_schema(
81 '::DBI::Replicated' => {
82 balancer_type=>'::Random',
84 auto_validate_every=>100,
85 master_read_weight => 1
95 sub get_schema_by_connect_info {
96 DBICTest->init_schema(
98 storage_type=> '::DBI::Replicated',
99 balancer_type=>'::Random',
101 auto_validate_every=>100,
102 master_read_weight => 1
110 sub generate_replicant_connect_info {}
114 ## --------------------------------------------------------------------- ##
115 ## Add a connect_info option to test option merging.
116 ## --------------------------------------------------------------------- ##
118 package DBIx::Class::Storage::DBI::Replicated;
122 __PACKAGE__->meta->make_mutable;
124 around connect_info => sub {
125 my ($next, $self, $info) = @_;
126 $info->[3]{master_option} = 1;
130 __PACKAGE__->meta->make_immutable;
135 ## --------------------------------------------------------------------- ##
136 ## Subclass for when you are using SQLite for testing, this provides a fake
137 ## replication support.
138 ## --------------------------------------------------------------------- ##
140 package DBIx::Class::DBI::Replicated::TestReplication::SQLite;
144 use base 'DBIx::Class::DBI::Replicated::TestReplication';
146 __PACKAGE__->mk_accessors(qw/master_path slave_paths/);
148 ## Set the master path from DBICTest
151 my $class = shift @_;
152 my $self = $class->SUPER::new(@_);
154 $self->master_path( DBICTest->_sqlite_dbfilename );
156 File::Spec->catfile(qw/t var DBIxClass_slave1.db/),
157 File::Spec->catfile(qw/t var DBIxClass_slave2.db/),
163 ## Return an Array of ArrayRefs where each ArrayRef is suitable to use for
164 ## $storage->connect_info to be used for connecting replicants.
166 sub generate_replicant_connect_info {
170 } @{$self->slave_paths};
172 my @connect_infos = map { [$_,'','',{AutoCommit=>1}] } @dsn;
174 ## Make sure nothing is left over from a failed test
178 my $c = $connect_infos[0];
179 $connect_infos[0] = {
189 ## Do a 'good enough' replication by copying the master dbfile over each of
190 ## the slave dbfiles. If the master is SQLite we do this, otherwise we
191 ## just do a one second pause to let the slaves catch up.
195 foreach my $slave (@{$self->slave_paths}) {
196 copy($self->master_path, $slave);
200 ## Cleanup after ourselves. Unlink all gthe slave paths.
204 foreach my $slave (@{$self->slave_paths}) {
211 ## --------------------------------------------------------------------- ##
212 ## Subclass for when you are setting the databases via custom export vars
213 ## This is for when you have a replicating database setup that you are
214 ## going to test against. You'll need to define the correct $ENV and have
215 ## two slave databases to test against, as well as a replication system
216 ## that will replicate in less than 1 second.
217 ## --------------------------------------------------------------------- ##
219 package DBIx::Class::DBI::Replicated::TestReplication::Custom;
220 use base 'DBIx::Class::DBI::Replicated::TestReplication';
222 ## Return an Array of ArrayRefs where each ArrayRef is suitable to use for
223 ## $storage->connect_info to be used for connecting replicants.
225 sub generate_replicant_connect_info {
227 [$ENV{"DBICTEST_SLAVE0_DSN"}, $ENV{"DBICTEST_SLAVE0_DBUSER"}, $ENV{"DBICTEST_SLAVE0_DBPASS"}, {AutoCommit => 1}],
228 [$ENV{"DBICTEST_SLAVE1_DSN"}, $ENV{"DBICTEST_SLAVE1_DBUSER"}, $ENV{"DBICTEST_SLAVE1_DBPASS"}, {AutoCommit => 1}],
232 ## pause a bit to let the replication catch up
239 ## ----------------------------------------------------------------------------
240 ## Create an object and run some tests
241 ## ----------------------------------------------------------------------------
243 ## Thi first bunch of tests are basic, just make sure all the bits are behaving
245 my $replicated_class = DBICTest->has_custom_dsn ?
246 'DBIx::Class::DBI::Replicated::TestReplication::Custom' :
247 'DBIx::Class::DBI::Replicated::TestReplication::SQLite';
251 for my $method (qw/by_connect_info by_storage_type/) {
252 ok $replicated = $replicated_class->new($method)
253 => "Created a replication object $method";
255 isa_ok $replicated->schema
256 => 'DBIx::Class::Schema';
258 isa_ok $replicated->schema->storage
259 => 'DBIx::Class::Storage::DBI::Replicated';
261 isa_ok $replicated->schema->storage->balancer
262 => 'DBIx::Class::Storage::DBI::Replicated::Balancer::Random'
263 => 'configured balancer_type';
266 ok $replicated->schema->storage->meta
267 => 'has a meta object';
269 isa_ok $replicated->schema->storage->master
270 => 'DBIx::Class::Storage::DBI';
272 isa_ok $replicated->schema->storage->pool
273 => 'DBIx::Class::Storage::DBI::Replicated::Pool';
275 does_ok $replicated->schema->storage->balancer
276 => 'DBIx::Class::Storage::DBI::Replicated::Balancer';
278 ok my @replicant_connects = $replicated->generate_replicant_connect_info
279 => 'got replication connect information';
281 ok my @replicated_storages = $replicated->schema->storage->connect_replicants(@replicant_connects)
282 => 'Created some storages suitable for replicants';
285 $replicated->schema->storage->debug(1);
286 $replicated->schema->storage->debugcb(sub {
287 my ($op, $info) = @_;
288 ##warn "\n$op, $info\n";
292 dsn => ($info=~m/\[(.+)\]/)[0],
293 storage_type => $info=~m/REPLICANT/ ? 'REPLICANT' : 'MASTER',
297 ok my @all_storages = $replicated->schema->storage->all_storages
300 is scalar @all_storages,
302 => 'correct number of ->all_storages';
304 is ((grep $_->isa('DBIx::Class::Storage::DBI'), @all_storages),
306 => '->all_storages are correct type');
308 my @all_storage_opts =
309 grep { (reftype($_)||'') eq 'HASH' }
310 map @{ $_->_connect_info }, @all_storages;
312 is ((grep $_->{master_option}, @all_storage_opts),
314 => 'connect_info was merged from master to replicants');
316 my @replicant_names = keys %{ $replicated->schema->storage->replicants };
318 ok @replicant_names, "found replicant names @replicant_names";
320 ## Silence warning about not supporting the is_replicating method if using the
322 $replicated->schema->storage->debugobj->silence(1)
323 if first { m{^t/} } @replicant_names;
325 isa_ok $replicated->schema->storage->balancer->current_replicant
326 => 'DBIx::Class::Storage::DBI';
328 $replicated->schema->storage->debugobj->silence(0);
330 ok $replicated->schema->storage->pool->has_replicants
331 => 'does have replicants';
333 is $replicated->schema->storage->pool->num_replicants => 2
334 => 'has two replicants';
336 does_ok $replicated_storages[0]
337 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
339 does_ok $replicated_storages[1]
340 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
342 does_ok $replicated->schema->storage->replicants->{$replicant_names[0]}
343 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
345 does_ok $replicated->schema->storage->replicants->{$replicant_names[1]}
346 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
348 ## Add some info to the database
352 ->populate('Artist', [
353 [ qw/artistid name/ ],
354 [ 4, "Ozric Tentacles"],
357 is $debug{storage_type}, 'MASTER',
358 "got last query from a master: $debug{dsn}";
360 like $debug{info}, qr/INSERT/, 'Last was an insert';
362 ## Make sure all the slaves have the table definitions
364 $replicated->replicate;
365 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(1);
366 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(1);
368 ## Silence warning about not supporting the is_replicating method if using the
370 $replicated->schema->storage->debugobj->silence(1)
371 if first { m{^t/} } @replicant_names;
373 $replicated->schema->storage->pool->validate_replicants;
375 $replicated->schema->storage->debugobj->silence(0);
377 ## Make sure we can read the data.
379 ok my $artist1 = $replicated->schema->resultset('Artist')->find(4)
382 ## We removed testing here since master read weight is on, so we can't tell in
383 ## advance what storage to expect. We turn master read weight off a bit lower
384 ## is $debug{storage_type}, 'REPLICANT'
385 ## => "got last query from a replicant: $debug{dsn}, $debug{info}";
388 => 'DBICTest::Artist';
390 is $artist1->name, 'Ozric Tentacles'
391 => 'Found expected name for first result';
393 ## Check that master_read_weight is honored
395 no warnings qw/once redefine/;
398 *DBIx::Class::Storage::DBI::Replicated::Balancer::Random::_random_number =
401 $replicated->schema->storage->balancer->increment_storage;
403 is $replicated->schema->storage->balancer->current_replicant,
404 $replicated->schema->storage->master
405 => 'master_read_weight is honored';
407 ## turn it off for the duration of the test
408 $replicated->schema->storage->balancer->master_read_weight(0);
409 $replicated->schema->storage->balancer->increment_storage;
412 ## Add some new rows that only the master will have This is because
413 ## we overload any type of write operation so that is must hit the master
418 ->populate('Artist', [
419 [ qw/artistid name/ ],
420 [ 5, "Doom's Children"],
421 [ 6, "Dead On Arrival"],
425 is $debug{storage_type}, 'MASTER',
426 "got last query from a master: $debug{dsn}";
428 like $debug{info}, qr/INSERT/, 'Last was an insert';
430 ## Make sure all the slaves have the table definitions
431 $replicated->replicate;
433 ## Should find some data now
435 ok my $artist2 = $replicated->schema->resultset('Artist')->find(5)
438 is $debug{storage_type}, 'REPLICANT'
439 => "got last query from a replicant: $debug{dsn}";
442 => 'DBICTest::Artist';
444 is $artist2->name, "Doom's Children"
445 => 'Found expected name for first result';
447 ## What happens when we disconnect all the replicants?
449 is $replicated->schema->storage->pool->connected_replicants => 2
450 => "both replicants are connected";
452 $replicated->schema->storage->replicants->{$replicant_names[0]}->disconnect;
453 $replicated->schema->storage->replicants->{$replicant_names[1]}->disconnect;
455 is $replicated->schema->storage->pool->connected_replicants => 0
456 => "both replicants are now disconnected";
458 ## All these should pass, since the database should automatically reconnect
460 ok my $artist3 = $replicated->schema->resultset('Artist')->find(6)
461 => 'Still finding stuff.';
463 is $debug{storage_type}, 'REPLICANT'
464 => "got last query from a replicant: $debug{dsn}";
467 => 'DBICTest::Artist';
469 is $artist3->name, "Dead On Arrival"
470 => 'Found expected name for first result';
472 is $replicated->schema->storage->pool->connected_replicants => 1
473 => "At Least One replicant reconnected to handle the job";
475 ## What happens when we try to select something that doesn't exist?
477 ok ! $replicated->schema->resultset('Artist')->find(666)
478 => 'Correctly failed to find something.';
480 is $debug{storage_type}, 'REPLICANT'
481 => "got last query from a replicant: $debug{dsn}";
483 ## test the reliable option
487 $replicated->schema->storage->set_reliable_storage;
489 ok $replicated->schema->resultset('Artist')->find(2)
490 => 'Read from master 1';
492 is $debug{storage_type}, 'MASTER',
493 "got last query from a master: $debug{dsn}";
495 ok $replicated->schema->resultset('Artist')->find(5)
496 => 'Read from master 2';
498 is $debug{storage_type}, 'MASTER',
499 "got last query from a master: $debug{dsn}";
501 $replicated->schema->storage->set_balanced_storage;
503 ok $replicated->schema->resultset('Artist')->find(3)
504 => 'Read from replicant';
506 is $debug{storage_type}, 'REPLICANT',
507 "got last query from a replicant: $debug{dsn}";
510 ## Make sure when reliable goes out of scope, we are using replicants again
512 ok $replicated->schema->resultset('Artist')->find(1)
513 => 'back to replicant 1.';
515 is $debug{storage_type}, 'REPLICANT',
516 "got last query from a replicant: $debug{dsn}";
518 ok $replicated->schema->resultset('Artist')->find(2)
519 => 'back to replicant 2.';
521 is $debug{storage_type}, 'REPLICANT',
522 "got last query from a replicant: $debug{dsn}";
524 ## set all the replicants to inactive, and make sure the balancer falls back to
527 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(0);
528 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(0);
531 ## catch the fallback to master warning
532 open my $debugfh, '>', \my $fallback_warning;
533 my $oldfh = $replicated->schema->storage->debugfh;
534 $replicated->schema->storage->debugfh($debugfh);
536 ok $replicated->schema->resultset('Artist')->find(2)
537 => 'Fallback to master';
539 is $debug{storage_type}, 'MASTER',
540 "got last query from a master: $debug{dsn}";
542 like $fallback_warning, qr/falling back to master/
543 => 'emits falling back to master warning';
545 $replicated->schema->storage->debugfh($oldfh);
548 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(1);
549 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(1);
551 ## Silence warning about not supporting the is_replicating method if using the
553 $replicated->schema->storage->debugobj->silence(1)
554 if first { m{^t/} } @replicant_names;
556 $replicated->schema->storage->pool->validate_replicants;
558 $replicated->schema->storage->debugobj->silence(0);
560 ok $replicated->schema->resultset('Artist')->find(2)
561 => 'Returned to replicates';
563 is $debug{storage_type}, 'REPLICANT',
564 "got last query from a replicant: $debug{dsn}";
566 ## Getting slave status tests
569 ## We skip this tests unless you have a custom replicants, since the default
570 ## sqlite based replication tests don't support these functions.
572 skip 'Cannot Test Replicant Status on Non Replicating Database', 10
573 unless DBICTest->has_custom_dsn && $ENV{"DBICTEST_SLAVE0_DSN"};
575 $replicated->replicate; ## Give the slaves a chance to catchup.
577 ok $replicated->schema->storage->replicants->{$replicant_names[0]}->is_replicating
578 => 'Replicants are replicating';
580 is $replicated->schema->storage->replicants->{$replicant_names[0]}->lag_behind_master, 0
581 => 'Replicant is zero seconds behind master';
583 ## Test the validate replicants
585 $replicated->schema->storage->pool->validate_replicants;
587 is $replicated->schema->storage->pool->active_replicants, 2
588 => 'Still have 2 replicants after validation';
590 ## Force the replicants to fail the validate test by required their lag to
591 ## be negative (ie ahead of the master!)
593 $replicated->schema->storage->pool->maximum_lag(-10);
594 $replicated->schema->storage->pool->validate_replicants;
596 is $replicated->schema->storage->pool->active_replicants, 0
597 => 'No way a replicant be be ahead of the master';
599 ## Let's be fair to the replicants again. Let them lag up to 5
601 $replicated->schema->storage->pool->maximum_lag(5);
602 $replicated->schema->storage->pool->validate_replicants;
604 is $replicated->schema->storage->pool->active_replicants, 2
605 => 'Both replicants in good standing again';
607 ## Check auto validate
609 is $replicated->schema->storage->balancer->auto_validate_every, 100
610 => "Got the expected value for auto validate";
612 ## This will make sure we auto validatge everytime
613 $replicated->schema->storage->balancer->auto_validate_every(0);
615 ## set all the replicants to inactive, and make sure the balancer falls back to
618 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(0);
619 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(0);
621 ## Ok, now when we go to run a query, autovalidate SHOULD reconnect
623 is $replicated->schema->storage->pool->active_replicants => 0
624 => "both replicants turned off";
626 ok $replicated->schema->resultset('Artist')->find(5)
627 => 'replicant reactivated';
629 is $debug{storage_type}, 'REPLICANT',
630 "got last query from a replicant: $debug{dsn}";
632 is $replicated->schema->storage->pool->active_replicants => 2
633 => "both replicants reactivated";
636 ## Test the reliably callback
638 ok my $reliably = sub {
640 ok $replicated->schema->resultset('Artist')->find(5)
641 => 'replicant reactivated';
643 is $debug{storage_type}, 'MASTER',
644 "got last query from a master: $debug{dsn}";
646 } => 'created coderef properly';
648 $replicated->schema->storage->execute_reliably($reliably);
650 ## Try something with an error
652 ok my $unreliably = sub {
654 ok $replicated->schema->resultset('ArtistXX')->find(5)
655 => 'replicant reactivated';
657 } => 'created coderef properly';
659 throws_ok {$replicated->schema->storage->execute_reliably($unreliably)}
660 qr/Can't find source for ArtistXX/
661 => 'Bad coderef throws proper error';
663 ## Make sure replication came back
665 ok $replicated->schema->resultset('Artist')->find(3)
666 => 'replicant reactivated';
668 is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
670 ## make sure transactions are set to execute_reliably
672 ok my $transaction = sub {
678 ->populate('Artist', [
679 [ qw/artistid name/ ],
680 [ $id, "Children of the Grave"],
683 ok my $result = $replicated->schema->resultset('Artist')->find($id)
684 => "Found expected artist for $id";
686 is $debug{storage_type}, 'MASTER',
687 "got last query from a master: $debug{dsn}";
689 ok my $more = $replicated->schema->resultset('Artist')->find(1)
690 => 'Found expected artist again for 1';
692 is $debug{storage_type}, 'MASTER',
693 "got last query from a master: $debug{dsn}";
695 return ($result, $more);
697 } => 'Created a coderef properly';
699 ## Test the transaction with multi return
701 ok my @return = $replicated->schema->txn_do($transaction, 666)
702 => 'did transaction';
704 is $return[0]->id, 666
705 => 'first returned value is correct';
707 is $debug{storage_type}, 'MASTER',
708 "got last query from a master: $debug{dsn}";
711 => 'second returned value is correct';
713 is $debug{storage_type}, 'MASTER',
714 "got last query from a master: $debug{dsn}";
718 ## Test that asking for single return works
720 ok my @return = $replicated->schema->txn_do($transaction, 777)
721 => 'did transaction';
723 is $return[0]->id, 777
724 => 'first returned value is correct';
727 => 'second returned value is correct';
730 ## Test transaction returning a single value
733 ok my $result = $replicated->schema->txn_do(sub {
734 ok my $more = $replicated->schema->resultset('Artist')->find(1)
735 => 'found inside a transaction';
736 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
738 }) => 'successfully processed transaction';
741 => 'Got expected single result from transaction';
744 ## Make sure replication came back
746 ok $replicated->schema->resultset('Artist')->find(1)
747 => 'replicant reactivated';
749 is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
751 ## Test Discard changes
754 ok my $artist = $replicated->schema->resultset('Artist')->find(2)
755 => 'got an artist to test discard changes';
757 is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
759 ok $artist->get_from_storage({force_pool=>'master'})
760 => 'properly discard changes';
762 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
766 ## Test some edge cases, like trying to do a transaction inside a transaction, etc
769 ok my $result = $replicated->schema->txn_do(sub {
770 return $replicated->schema->txn_do(sub {
771 ok my $more = $replicated->schema->resultset('Artist')->find(1)
772 => 'found inside a transaction inside a transaction';
773 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
776 }) => 'successfully processed transaction';
779 => 'Got expected single result from transaction';
783 ok my $result = $replicated->schema->txn_do(sub {
784 return $replicated->schema->storage->execute_reliably(sub {
785 return $replicated->schema->txn_do(sub {
786 return $replicated->schema->storage->execute_reliably(sub {
787 ok my $more = $replicated->schema->resultset('Artist')->find(1)
788 => 'found inside crazy deep transactions and execute_reliably';
789 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
794 }) => 'successfully processed transaction';
797 => 'Got expected single result from transaction';
800 ## Test the force_pool resultset attribute.
803 ok my $artist_rs = $replicated->schema->resultset('Artist')
804 => 'got artist resultset';
806 ## Turn on Forced Pool Storage
807 ok my $reliable_artist_rs = $artist_rs->search(undef, {force_pool=>'master'})
808 => 'Created a resultset using force_pool storage';
810 ok my $artist = $reliable_artist_rs->find(2)
811 => 'got an artist result via force_pool storage';
813 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
816 ## Test the force_pool resultset attribute part two.
819 ok my $artist_rs = $replicated->schema->resultset('Artist')
820 => 'got artist resultset';
822 ## Turn on Forced Pool Storage
823 ok my $reliable_artist_rs = $artist_rs->search(undef, {force_pool=>$replicant_names[0]})
824 => 'Created a resultset using force_pool storage';
826 ok my $artist = $reliable_artist_rs->find(2)
827 => 'got an artist result via force_pool storage';
829 is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
831 ## Delete the old database files
832 $replicated->cleanup;