7 use List::Util 'first';
8 use Scalar::Util 'reftype';
13 eval "use DBIx::Class::Storage::DBI::Replicated; use Test::Moose";
14 plan skip_all => "Deps not installed: $@" if $@;
17 use_ok 'DBIx::Class::Storage::DBI::Replicated::Pool';
18 use_ok 'DBIx::Class::Storage::DBI::Replicated::Balancer';
19 use_ok 'DBIx::Class::Storage::DBI::Replicated::Replicant';
20 use_ok 'DBIx::Class::Storage::DBI::Replicated';
24 diag "Using Moose version $Moose::VERSION and MooseX::Types version $MooseX::Types::VERSION";
28 This is a test of the replicated storage system. This will work in one of
29 two ways, either it was try to fake replication with a couple of SQLite DBs
30 and creative use of copy, or if you define a couple of %ENV vars correctly
31 will try to test those. If you do that, it will assume the setup is properly
32 replicating. Your results may vary, but I have demonstrated this to work with
33 mysql native replication.
38 ## ----------------------------------------------------------------------------
39 ## Build a class to hold all our required testing data and methods.
40 ## ----------------------------------------------------------------------------
44 ## --------------------------------------------------------------------- ##
45 ## Create an object to contain your replicated stuff.
46 ## --------------------------------------------------------------------- ##
48 package DBIx::Class::DBI::Replicated::TestReplication;
51 use base qw/Class::Accessor::Fast/;
53 __PACKAGE__->mk_accessors( qw/schema/ );
55 ## Initialize the object
58 my ($class, $schema_method) = (shift, shift);
59 my $self = $class->SUPER::new(@_);
61 $self->schema( $self->init_schema($schema_method) );
65 ## Get the Schema and set the replication storage type
68 # current SQLT SQLite producer does not handle DROP TABLE IF EXISTS, trap warnings here
69 local $SIG{__WARN__} = sub { warn @_ unless $_[0] =~ /no such table.+DROP TABLE/ };
71 my ($class, $schema_method) = @_;
73 my $method = "get_schema_$schema_method";
74 my $schema = $class->$method;
79 sub get_schema_by_storage_type {
80 DBICTest->init_schema(
83 '::DBI::Replicated' => {
84 balancer_type=>'::Random',
86 auto_validate_every=>100,
87 master_read_weight => 1
97 sub get_schema_by_connect_info {
98 DBICTest->init_schema(
100 storage_type=> '::DBI::Replicated',
101 balancer_type=>'::Random',
103 auto_validate_every=>100,
104 master_read_weight => 1
112 sub generate_replicant_connect_info {}
116 ## --------------------------------------------------------------------- ##
117 ## Add a connect_info option to test option merging.
118 ## --------------------------------------------------------------------- ##
120 package DBIx::Class::Storage::DBI::Replicated;
124 __PACKAGE__->meta->make_mutable;
126 around connect_info => sub {
127 my ($next, $self, $info) = @_;
128 $info->[3]{master_option} = 1;
132 __PACKAGE__->meta->make_immutable;
137 ## --------------------------------------------------------------------- ##
138 ## Subclass for when you are using SQLite for testing, this provides a fake
139 ## replication support.
140 ## --------------------------------------------------------------------- ##
142 package DBIx::Class::DBI::Replicated::TestReplication::SQLite;
146 use base 'DBIx::Class::DBI::Replicated::TestReplication';
148 __PACKAGE__->mk_accessors(qw/master_path slave_paths/);
150 ## Set the master path from DBICTest
153 my $class = shift @_;
154 my $self = $class->SUPER::new(@_);
156 $self->master_path( DBICTest->_sqlite_dbfilename );
158 File::Spec->catfile(qw/t var DBIxClass_slave1.db/),
159 File::Spec->catfile(qw/t var DBIxClass_slave2.db/),
165 ## Return an Array of ArrayRefs where each ArrayRef is suitable to use for
166 ## $storage->connect_info to be used for connecting replicants.
168 sub generate_replicant_connect_info {
172 } @{$self->slave_paths};
174 my @connect_infos = map { [$_,'','',{AutoCommit=>1}] } @dsn;
176 ## Make sure nothing is left over from a failed test
180 my $c = $connect_infos[0];
181 $connect_infos[0] = {
191 ## Do a 'good enough' replication by copying the master dbfile over each of
192 ## the slave dbfiles. If the master is SQLite we do this, otherwise we
193 ## just do a one second pause to let the slaves catch up.
197 foreach my $slave (@{$self->slave_paths}) {
198 copy($self->master_path, $slave);
202 ## Cleanup after ourselves. Unlink all gthe slave paths.
206 foreach my $slave (@{$self->slave_paths}) {
213 ## --------------------------------------------------------------------- ##
214 ## Subclass for when you are setting the databases via custom export vars
215 ## This is for when you have a replicating database setup that you are
216 ## going to test against. You'll need to define the correct $ENV and have
217 ## two slave databases to test against, as well as a replication system
218 ## that will replicate in less than 1 second.
219 ## --------------------------------------------------------------------- ##
221 package DBIx::Class::DBI::Replicated::TestReplication::Custom;
222 use base 'DBIx::Class::DBI::Replicated::TestReplication';
224 ## Return an Array of ArrayRefs where each ArrayRef is suitable to use for
225 ## $storage->connect_info to be used for connecting replicants.
227 sub generate_replicant_connect_info {
229 [$ENV{"DBICTEST_SLAVE0_DSN"}, $ENV{"DBICTEST_SLAVE0_DBUSER"}, $ENV{"DBICTEST_SLAVE0_DBPASS"}, {AutoCommit => 1}],
230 [$ENV{"DBICTEST_SLAVE1_DSN"}, $ENV{"DBICTEST_SLAVE1_DBUSER"}, $ENV{"DBICTEST_SLAVE1_DBPASS"}, {AutoCommit => 1}],
234 ## pause a bit to let the replication catch up
241 ## ----------------------------------------------------------------------------
242 ## Create an object and run some tests
243 ## ----------------------------------------------------------------------------
245 ## Thi first bunch of tests are basic, just make sure all the bits are behaving
247 my $replicated_class = DBICTest->has_custom_dsn ?
248 'DBIx::Class::DBI::Replicated::TestReplication::Custom' :
249 'DBIx::Class::DBI::Replicated::TestReplication::SQLite';
253 for my $method (qw/by_connect_info by_storage_type/) {
255 ok $replicated = $replicated_class->new($method)
256 => "Created a replication object $method";
258 isa_ok $replicated->schema
259 => 'DBIx::Class::Schema';
261 isa_ok $replicated->schema->storage
262 => 'DBIx::Class::Storage::DBI::Replicated';
264 isa_ok $replicated->schema->storage->balancer
265 => 'DBIx::Class::Storage::DBI::Replicated::Balancer::Random'
266 => 'configured balancer_type';
269 ok $replicated->schema->storage->meta
270 => 'has a meta object';
272 isa_ok $replicated->schema->storage->master
273 => 'DBIx::Class::Storage::DBI';
275 isa_ok $replicated->schema->storage->pool
276 => 'DBIx::Class::Storage::DBI::Replicated::Pool';
278 does_ok $replicated->schema->storage->balancer
279 => 'DBIx::Class::Storage::DBI::Replicated::Balancer';
281 ok my @replicant_connects = $replicated->generate_replicant_connect_info
282 => 'got replication connect information';
284 ok my @replicated_storages = $replicated->schema->storage->connect_replicants(@replicant_connects)
285 => 'Created some storages suitable for replicants';
288 $replicated->schema->storage->debug(1);
289 $replicated->schema->storage->debugcb(sub {
290 my ($op, $info) = @_;
291 ##warn "\n$op, $info\n";
295 dsn => ($info=~m/\[(.+)\]/)[0],
296 storage_type => $info=~m/REPLICANT/ ? 'REPLICANT' : 'MASTER',
300 ok my @all_storages = $replicated->schema->storage->all_storages
303 is scalar @all_storages,
305 => 'correct number of ->all_storages';
307 is ((grep $_->isa('DBIx::Class::Storage::DBI'), @all_storages),
309 => '->all_storages are correct type');
311 my @all_storage_opts =
312 grep { (reftype($_)||'') eq 'HASH' }
313 map @{ $_->_connect_info }, @all_storages;
315 is ((grep $_->{master_option}, @all_storage_opts),
317 => 'connect_info was merged from master to replicants');
319 my @replicant_names = keys %{ $replicated->schema->storage->replicants };
321 ok @replicant_names, "found replicant names @replicant_names";
323 ## Silence warning about not supporting the is_replicating method if using the
325 $replicated->schema->storage->debugobj->silence(1)
326 if first { m{^t/} } @replicant_names;
328 isa_ok $replicated->schema->storage->balancer->current_replicant
329 => 'DBIx::Class::Storage::DBI';
331 $replicated->schema->storage->debugobj->silence(0);
333 ok $replicated->schema->storage->pool->has_replicants
334 => 'does have replicants';
336 is $replicated->schema->storage->pool->num_replicants => 2
337 => 'has two replicants';
339 does_ok $replicated_storages[0]
340 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
342 does_ok $replicated_storages[1]
343 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
345 does_ok $replicated->schema->storage->replicants->{$replicant_names[0]}
346 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
348 does_ok $replicated->schema->storage->replicants->{$replicant_names[1]}
349 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
351 ## Add some info to the database
355 ->populate('Artist', [
356 [ qw/artistid name/ ],
357 [ 4, "Ozric Tentacles"],
360 is $debug{storage_type}, 'MASTER',
361 "got last query from a master: $debug{dsn}";
363 like $debug{info}, qr/INSERT/, 'Last was an insert';
365 ## Make sure all the slaves have the table definitions
367 $replicated->replicate;
368 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(1);
369 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(1);
371 ## Silence warning about not supporting the is_replicating method if using the
373 $replicated->schema->storage->debugobj->silence(1)
374 if first { m{^t/} } @replicant_names;
376 $replicated->schema->storage->pool->validate_replicants;
378 $replicated->schema->storage->debugobj->silence(0);
380 ## Make sure we can read the data.
382 ok my $artist1 = $replicated->schema->resultset('Artist')->find(4)
385 ## We removed testing here since master read weight is on, so we can't tell in
386 ## advance what storage to expect. We turn master read weight off a bit lower
387 ## is $debug{storage_type}, 'REPLICANT'
388 ## => "got last query from a replicant: $debug{dsn}, $debug{info}";
391 => 'DBICTest::Artist';
393 is $artist1->name, 'Ozric Tentacles'
394 => 'Found expected name for first result';
396 ## Check that master_read_weight is honored
398 no warnings qw/once redefine/;
401 *DBIx::Class::Storage::DBI::Replicated::Balancer::Random::_random_number =
404 $replicated->schema->storage->balancer->increment_storage;
406 is $replicated->schema->storage->balancer->current_replicant,
407 $replicated->schema->storage->master
408 => 'master_read_weight is honored';
410 ## turn it off for the duration of the test
411 $replicated->schema->storage->balancer->master_read_weight(0);
412 $replicated->schema->storage->balancer->increment_storage;
415 ## Add some new rows that only the master will have This is because
416 ## we overload any type of write operation so that is must hit the master
421 ->populate('Artist', [
422 [ qw/artistid name/ ],
423 [ 5, "Doom's Children"],
424 [ 6, "Dead On Arrival"],
428 is $debug{storage_type}, 'MASTER',
429 "got last query from a master: $debug{dsn}";
431 like $debug{info}, qr/INSERT/, 'Last was an insert';
433 ## Make sure all the slaves have the table definitions
434 $replicated->replicate;
436 ## Should find some data now
438 ok my $artist2 = $replicated->schema->resultset('Artist')->find(5)
441 is $debug{storage_type}, 'REPLICANT'
442 => "got last query from a replicant: $debug{dsn}";
445 => 'DBICTest::Artist';
447 is $artist2->name, "Doom's Children"
448 => 'Found expected name for first result';
450 ## What happens when we disconnect all the replicants?
452 is $replicated->schema->storage->pool->connected_replicants => 2
453 => "both replicants are connected";
455 $replicated->schema->storage->replicants->{$replicant_names[0]}->disconnect;
456 $replicated->schema->storage->replicants->{$replicant_names[1]}->disconnect;
458 is $replicated->schema->storage->pool->connected_replicants => 0
459 => "both replicants are now disconnected";
461 ## All these should pass, since the database should automatically reconnect
463 ok my $artist3 = $replicated->schema->resultset('Artist')->find(6)
464 => 'Still finding stuff.';
466 is $debug{storage_type}, 'REPLICANT'
467 => "got last query from a replicant: $debug{dsn}";
470 => 'DBICTest::Artist';
472 is $artist3->name, "Dead On Arrival"
473 => 'Found expected name for first result';
475 is $replicated->schema->storage->pool->connected_replicants => 1
476 => "At Least One replicant reconnected to handle the job";
478 ## What happens when we try to select something that doesn't exist?
480 ok ! $replicated->schema->resultset('Artist')->find(666)
481 => 'Correctly failed to find something.';
483 is $debug{storage_type}, 'REPLICANT'
484 => "got last query from a replicant: $debug{dsn}";
486 ## test the reliable option
490 $replicated->schema->storage->set_reliable_storage;
492 ok $replicated->schema->resultset('Artist')->find(2)
493 => 'Read from master 1';
495 is $debug{storage_type}, 'MASTER',
496 "got last query from a master: $debug{dsn}";
498 ok $replicated->schema->resultset('Artist')->find(5)
499 => 'Read from master 2';
501 is $debug{storage_type}, 'MASTER',
502 "got last query from a master: $debug{dsn}";
504 $replicated->schema->storage->set_balanced_storage;
506 ok $replicated->schema->resultset('Artist')->find(3)
507 => 'Read from replicant';
509 is $debug{storage_type}, 'REPLICANT',
510 "got last query from a replicant: $debug{dsn}";
513 ## Make sure when reliable goes out of scope, we are using replicants again
515 ok $replicated->schema->resultset('Artist')->find(1)
516 => 'back to replicant 1.';
518 is $debug{storage_type}, 'REPLICANT',
519 "got last query from a replicant: $debug{dsn}";
521 ok $replicated->schema->resultset('Artist')->find(2)
522 => 'back to replicant 2.';
524 is $debug{storage_type}, 'REPLICANT',
525 "got last query from a replicant: $debug{dsn}";
527 ## set all the replicants to inactive, and make sure the balancer falls back to
530 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(0);
531 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(0);
534 ## catch the fallback to master warning
535 open my $debugfh, '>', \my $fallback_warning;
536 my $oldfh = $replicated->schema->storage->debugfh;
537 $replicated->schema->storage->debugfh($debugfh);
539 ok $replicated->schema->resultset('Artist')->find(2)
540 => 'Fallback to master';
542 is $debug{storage_type}, 'MASTER',
543 "got last query from a master: $debug{dsn}";
545 like $fallback_warning, qr/falling back to master/
546 => 'emits falling back to master warning';
548 $replicated->schema->storage->debugfh($oldfh);
551 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(1);
552 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(1);
554 ## Silence warning about not supporting the is_replicating method if using the
556 $replicated->schema->storage->debugobj->silence(1)
557 if first { m{^t/} } @replicant_names;
559 $replicated->schema->storage->pool->validate_replicants;
561 $replicated->schema->storage->debugobj->silence(0);
563 ok $replicated->schema->resultset('Artist')->find(2)
564 => 'Returned to replicates';
566 is $debug{storage_type}, 'REPLICANT',
567 "got last query from a replicant: $debug{dsn}";
569 ## Getting slave status tests
572 ## We skip this tests unless you have a custom replicants, since the default
573 ## sqlite based replication tests don't support these functions.
575 skip 'Cannot Test Replicant Status on Non Replicating Database', 10
576 unless DBICTest->has_custom_dsn && $ENV{"DBICTEST_SLAVE0_DSN"};
578 $replicated->replicate; ## Give the slaves a chance to catchup.
580 ok $replicated->schema->storage->replicants->{$replicant_names[0]}->is_replicating
581 => 'Replicants are replicating';
583 is $replicated->schema->storage->replicants->{$replicant_names[0]}->lag_behind_master, 0
584 => 'Replicant is zero seconds behind master';
586 ## Test the validate replicants
588 $replicated->schema->storage->pool->validate_replicants;
590 is $replicated->schema->storage->pool->active_replicants, 2
591 => 'Still have 2 replicants after validation';
593 ## Force the replicants to fail the validate test by required their lag to
594 ## be negative (ie ahead of the master!)
596 $replicated->schema->storage->pool->maximum_lag(-10);
597 $replicated->schema->storage->pool->validate_replicants;
599 is $replicated->schema->storage->pool->active_replicants, 0
600 => 'No way a replicant be be ahead of the master';
602 ## Let's be fair to the replicants again. Let them lag up to 5
604 $replicated->schema->storage->pool->maximum_lag(5);
605 $replicated->schema->storage->pool->validate_replicants;
607 is $replicated->schema->storage->pool->active_replicants, 2
608 => 'Both replicants in good standing again';
610 ## Check auto validate
612 is $replicated->schema->storage->balancer->auto_validate_every, 100
613 => "Got the expected value for auto validate";
615 ## This will make sure we auto validatge everytime
616 $replicated->schema->storage->balancer->auto_validate_every(0);
618 ## set all the replicants to inactive, and make sure the balancer falls back to
621 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(0);
622 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(0);
624 ## Ok, now when we go to run a query, autovalidate SHOULD reconnect
626 is $replicated->schema->storage->pool->active_replicants => 0
627 => "both replicants turned off";
629 ok $replicated->schema->resultset('Artist')->find(5)
630 => 'replicant reactivated';
632 is $debug{storage_type}, 'REPLICANT',
633 "got last query from a replicant: $debug{dsn}";
635 is $replicated->schema->storage->pool->active_replicants => 2
636 => "both replicants reactivated";
639 ## Test the reliably callback
641 ok my $reliably = sub {
643 ok $replicated->schema->resultset('Artist')->find(5)
644 => 'replicant reactivated';
646 is $debug{storage_type}, 'MASTER',
647 "got last query from a master: $debug{dsn}";
649 } => 'created coderef properly';
651 $replicated->schema->storage->execute_reliably($reliably);
653 ## Try something with an error
655 ok my $unreliably = sub {
657 ok $replicated->schema->resultset('ArtistXX')->find(5)
658 => 'replicant reactivated';
660 } => 'created coderef properly';
662 throws_ok {$replicated->schema->storage->execute_reliably($unreliably)}
663 qr/Can't find source for ArtistXX/
664 => 'Bad coderef throws proper error';
666 ## Make sure replication came back
668 ok $replicated->schema->resultset('Artist')->find(3)
669 => 'replicant reactivated';
671 is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
673 ## make sure transactions are set to execute_reliably
675 ok my $transaction = sub {
681 ->populate('Artist', [
682 [ qw/artistid name/ ],
683 [ $id, "Children of the Grave"],
686 ok my $result = $replicated->schema->resultset('Artist')->find($id)
687 => "Found expected artist for $id";
689 is $debug{storage_type}, 'MASTER',
690 "got last query from a master: $debug{dsn}";
692 ok my $more = $replicated->schema->resultset('Artist')->find(1)
693 => 'Found expected artist again for 1';
695 is $debug{storage_type}, 'MASTER',
696 "got last query from a master: $debug{dsn}";
698 return ($result, $more);
700 } => 'Created a coderef properly';
702 ## Test the transaction with multi return
704 ok my @return = $replicated->schema->txn_do($transaction, 666)
705 => 'did transaction';
707 is $return[0]->id, 666
708 => 'first returned value is correct';
710 is $debug{storage_type}, 'MASTER',
711 "got last query from a master: $debug{dsn}";
714 => 'second returned value is correct';
716 is $debug{storage_type}, 'MASTER',
717 "got last query from a master: $debug{dsn}";
721 ## Test that asking for single return works
723 ok my @return = $replicated->schema->txn_do($transaction, 777)
724 => 'did transaction';
726 is $return[0]->id, 777
727 => 'first returned value is correct';
730 => 'second returned value is correct';
733 ## Test transaction returning a single value
736 ok my $result = $replicated->schema->txn_do(sub {
737 ok my $more = $replicated->schema->resultset('Artist')->find(1)
738 => 'found inside a transaction';
739 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
741 }) => 'successfully processed transaction';
744 => 'Got expected single result from transaction';
747 ## Make sure replication came back
749 ok $replicated->schema->resultset('Artist')->find(1)
750 => 'replicant reactivated';
752 is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
754 ## Test Discard changes
757 ok my $artist = $replicated->schema->resultset('Artist')->find(2)
758 => 'got an artist to test discard changes';
760 is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
762 ok $artist->get_from_storage({force_pool=>'master'})
763 => 'properly discard changes';
765 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
767 ok $artist->discard_changes({force_pool=>'master'})
768 => 'properly called discard_changes against master (manual attrs)';
770 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
772 ok $artist->discard_changes()
773 => 'properly called discard_changes against master (default attrs)';
775 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
777 ok $artist->discard_changes({force_pool=>$replicant_names[0]})
778 => 'properly able to override the default attributes';
780 is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}"
783 ## Test some edge cases, like trying to do a transaction inside a transaction, etc
786 ok my $result = $replicated->schema->txn_do(sub {
787 return $replicated->schema->txn_do(sub {
788 ok my $more = $replicated->schema->resultset('Artist')->find(1)
789 => 'found inside a transaction inside a transaction';
790 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
793 }) => 'successfully processed transaction';
796 => 'Got expected single result from transaction';
800 ok my $result = $replicated->schema->txn_do(sub {
801 return $replicated->schema->storage->execute_reliably(sub {
802 return $replicated->schema->txn_do(sub {
803 return $replicated->schema->storage->execute_reliably(sub {
804 ok my $more = $replicated->schema->resultset('Artist')->find(1)
805 => 'found inside crazy deep transactions and execute_reliably';
806 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
811 }) => 'successfully processed transaction';
814 => 'Got expected single result from transaction';
817 ## Test the force_pool resultset attribute.
820 ok my $artist_rs = $replicated->schema->resultset('Artist')
821 => 'got artist resultset';
823 ## Turn on Forced Pool Storage
824 ok my $reliable_artist_rs = $artist_rs->search(undef, {force_pool=>'master'})
825 => 'Created a resultset using force_pool storage';
827 ok my $artist = $reliable_artist_rs->find(2)
828 => 'got an artist result via force_pool storage';
830 is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
833 ## Test the force_pool resultset attribute part two.
836 ok my $artist_rs = $replicated->schema->resultset('Artist')
837 => 'got artist resultset';
839 ## Turn on Forced Pool Storage
840 ok my $reliable_artist_rs = $artist_rs->search(undef, {force_pool=>$replicant_names[0]})
841 => 'Created a resultset using force_pool storage';
843 ok my $artist = $reliable_artist_rs->find(2)
844 => 'got an artist result via force_pool storage';
846 is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
848 ## Delete the old database files
849 $replicated->cleanup;