::DBI::Replicated - fix fallback to master, test for the warning, other cleanups
[dbsrgits/DBIx-Class.git] / t / 93storage_replication.t
1 use strict;
2 use warnings;
3 use lib qw(t/lib);
4 use Test::More;
5 use Test::Exception;
6 use DBICTest;
7 use List::Util 'first';
8 use Scalar::Util 'reftype';
9 use IO::Handle;
10
11 BEGIN {
12     eval "use DBIx::Class::Storage::DBI::Replicated; use Test::Moose";
13     plan $@
14         ? ( skip_all => "Deps not installed: $@" )
15         : ( tests => 90 );
16 }
17
18 use_ok 'DBIx::Class::Storage::DBI::Replicated::Pool';
19 use_ok 'DBIx::Class::Storage::DBI::Replicated::Balancer';
20 use_ok 'DBIx::Class::Storage::DBI::Replicated::Replicant';
21 use_ok 'DBIx::Class::Storage::DBI::Replicated';
22
23 =head1 HOW TO USE
24
25     This is a test of the replicated storage system.  This will work in one of
26     two ways, either it was try to fake replication with a couple of SQLite DBs
27     and creative use of copy, or if you define a couple of %ENV vars correctly
28     will try to test those.  If you do that, it will assume the setup is properly
29     replicating.  Your results may vary, but I have demonstrated this to work with
30     mysql native replication.
31     
32 =cut
33
34
35 ## ----------------------------------------------------------------------------
36 ## Build a class to hold all our required testing data and methods.
37 ## ----------------------------------------------------------------------------
38
39 TESTSCHEMACLASSES: {
40
41     ## --------------------------------------------------------------------- ##
42     ## Create an object to contain your replicated stuff.
43     ## --------------------------------------------------------------------- ##
44     
45     package DBIx::Class::DBI::Replicated::TestReplication;
46    
47     use DBICTest;
48     use base qw/Class::Accessor::Fast/;
49     
50     __PACKAGE__->mk_accessors( qw/schema/ );
51
52     ## Initialize the object
53     
54         sub new {
55             my ($class, $schema_method) = (shift, shift);
56             my $self = $class->SUPER::new(@_);
57         
58             $self->schema( $self->init_schema($schema_method) );
59             return $self;
60         }
61     
62     ## Get the Schema and set the replication storage type
63     
64     sub init_schema {
65         # current SQLT SQLite producer does not handle DROP TABLE IF EXISTS, trap warnings here
66         local $SIG{__WARN__} = sub { warn @_ unless $_[0] =~ /no such table.+DROP TABLE/ };
67
68         my ($class, $schema_method) = @_;
69
70         my $method = "get_schema_$schema_method";
71         my $schema = $class->$method;
72
73         return $schema;
74     }
75
76     sub get_schema_by_storage_type {
77       DBICTest->init_schema(
78         sqlite_use_file => 1,
79         storage_type=>{
80           '::DBI::Replicated' => {
81             balancer_type=>'::Random',
82             balancer_args=>{
83               auto_validate_every=>100,
84               master_read_weight => 1
85             },
86           }
87         },
88         deploy_args=>{
89           add_drop_table => 1,
90         },
91       );
92     }
93
94     sub get_schema_by_connect_info {
95       DBICTest->init_schema(
96         sqlite_use_file => 1,
97         storage_type=> '::DBI::Replicated',
98         balancer_type=>'::Random',
99         balancer_args=> {
100           auto_validate_every=>100,
101           master_read_weight => 1
102         },
103         deploy_args=>{
104           add_drop_table => 1,
105         },
106       );
107     }
108
109     sub generate_replicant_connect_info {}
110     sub replicate {}
111     sub cleanup {}
112
113     ## --------------------------------------------------------------------- ##
114     ## Add a connect_info option to test option merging.
115     ## --------------------------------------------------------------------- ##
116     {
117     package DBIx::Class::Storage::DBI::Replicated;
118
119     use Moose;
120
121     __PACKAGE__->meta->make_mutable;
122
123     around connect_info => sub {
124       my ($next, $self, $info) = @_;
125       $info->[3]{master_option} = 1;
126       $self->$next($info);
127     };
128
129     __PACKAGE__->meta->make_immutable;
130
131     no Moose;
132     }
133   
134     ## --------------------------------------------------------------------- ##
135     ## Subclass for when you are using SQLite for testing, this provides a fake
136     ## replication support.
137     ## --------------------------------------------------------------------- ##
138         
139     package DBIx::Class::DBI::Replicated::TestReplication::SQLite;
140
141     use DBICTest;
142     use File::Copy;    
143     use base 'DBIx::Class::DBI::Replicated::TestReplication';
144     
145     __PACKAGE__->mk_accessors( qw/master_path slave_paths/ );
146     
147     ## Set the mastep path from DBICTest
148     
149         sub new {
150             my $class = shift @_;
151             my $self = $class->SUPER::new(@_);
152         
153             $self->master_path( DBICTest->_sqlite_dbfilename );
154             $self->slave_paths([
155             "t/var/DBIxClass_slave1.db",
156             "t/var/DBIxClass_slave2.db",    
157         ]);
158         
159             return $self;
160         }    
161         
162     ## Return an Array of ArrayRefs where each ArrayRef is suitable to use for
163     ## $storage->connect_info to be used for connecting replicants.
164     
165     sub generate_replicant_connect_info {
166         my $self = shift @_;
167         my @dsn = map {
168             "dbi:SQLite:${_}";
169         } @{$self->slave_paths};
170         
171         my @connect_infos = map { [$_,'','',{AutoCommit=>1}] } @dsn;
172
173     # try a hashref too
174         my $c = $connect_infos[0];
175         $connect_infos[0] = {
176           dsn => $c->[0],
177           user => $c->[1],
178           password => $c->[2],
179           %{ $c->[3] }
180         };
181
182         @connect_infos
183     }
184
185     ## Do a 'good enough' replication by copying the master dbfile over each of
186     ## the slave dbfiles.  If the master is SQLite we do this, otherwise we
187     ## just do a one second pause to let the slaves catch up.
188     
189     sub replicate {
190         my $self = shift @_;
191         foreach my $slave (@{$self->slave_paths}) {
192             copy($self->master_path, $slave);
193         }
194     }
195     
196     ## Cleanup after ourselves.  Unlink all gthe slave paths.
197     
198     sub cleanup {
199         my $self = shift @_;
200         foreach my $slave (@{$self->slave_paths}) {
201             unlink $slave;
202         }     
203     }
204     
205     ## --------------------------------------------------------------------- ##
206     ## Subclass for when you are setting the databases via custom export vars
207     ## This is for when you have a replicating database setup that you are
208     ## going to test against.  You'll need to define the correct $ENV and have
209     ## two slave databases to test against, as well as a replication system
210     ## that will replicate in less than 1 second.
211     ## --------------------------------------------------------------------- ##
212         
213     package DBIx::Class::DBI::Replicated::TestReplication::Custom; 
214     use base 'DBIx::Class::DBI::Replicated::TestReplication';
215     
216     ## Return an Array of ArrayRefs where each ArrayRef is suitable to use for
217     ## $storage->connect_info to be used for connecting replicants.
218     
219     sub generate_replicant_connect_info { 
220         return (
221             [$ENV{"DBICTEST_SLAVE0_DSN"}, $ENV{"DBICTEST_SLAVE0_DBUSER"}, $ENV{"DBICTEST_SLAVE0_DBPASS"}, {AutoCommit => 1}],
222             [$ENV{"DBICTEST_SLAVE1_DSN"}, $ENV{"DBICTEST_SLAVE1_DBUSER"}, $ENV{"DBICTEST_SLAVE1_DBPASS"}, {AutoCommit => 1}],           
223         );
224     }
225     
226     ## pause a bit to let the replication catch up 
227     
228     sub replicate {
229         sleep 1;
230     } 
231 }
232
233 ## ----------------------------------------------------------------------------
234 ## Create an object and run some tests
235 ## ----------------------------------------------------------------------------
236
237 ## Thi first bunch of tests are basic, just make sure all the bits are behaving
238
239 my $replicated_class = DBICTest->has_custom_dsn ?
240     'DBIx::Class::DBI::Replicated::TestReplication::Custom' :
241     'DBIx::Class::DBI::Replicated::TestReplication::SQLite';
242
243 my $replicated;
244
245 for my $method (qw/by_connect_info by_storage_type/) {
246   ok $replicated = $replicated_class->new($method)
247       => "Created a replication object $method";
248       
249   isa_ok $replicated->schema
250       => 'DBIx::Class::Schema';
251       
252   isa_ok $replicated->schema->storage
253       => 'DBIx::Class::Storage::DBI::Replicated';
254
255   isa_ok $replicated->schema->storage->balancer
256       => 'DBIx::Class::Storage::DBI::Replicated::Balancer::Random'
257       => 'configured balancer_type';
258 }
259
260 ok $replicated->schema->storage->meta
261     => 'has a meta object';
262     
263 isa_ok $replicated->schema->storage->master
264     => 'DBIx::Class::Storage::DBI';
265     
266 isa_ok $replicated->schema->storage->pool
267     => 'DBIx::Class::Storage::DBI::Replicated::Pool';
268     
269 does_ok $replicated->schema->storage->balancer
270     => 'DBIx::Class::Storage::DBI::Replicated::Balancer'; 
271
272 ok my @replicant_connects = $replicated->generate_replicant_connect_info
273     => 'got replication connect information';
274
275 ok my @replicated_storages = $replicated->schema->storage->connect_replicants(@replicant_connects)
276     => 'Created some storages suitable for replicants';
277
278 ok my @all_storages = $replicated->schema->storage->all_storages
279     => '->all_storages';
280
281 is scalar @all_storages,
282     3
283     => 'correct number of ->all_storages';
284
285 is ((grep $_->isa('DBIx::Class::Storage::DBI'), @all_storages),
286     3
287     => '->all_storages are correct type');
288
289 my @all_storage_opts =
290   grep { (reftype($_)||'') eq 'HASH' }
291     map @{ $_->_connect_info }, @all_storages;
292
293 is ((grep $_->{master_option}, @all_storage_opts),
294     3
295     => 'connect_info was merged from master to replicants');
296  
297 my @replicant_names = keys %{ $replicated->schema->storage->replicants };
298
299 ## Silence warning about not supporting the is_replicating method if using the
300 ## sqlite dbs.
301 $replicated->schema->storage->debugobj->silence(1)
302   if first { m{^t/} } @replicant_names;
303    
304 isa_ok $replicated->schema->storage->balancer->current_replicant
305     => 'DBIx::Class::Storage::DBI'; 
306
307 $replicated->schema->storage->debugobj->silence(0);
308
309 ok $replicated->schema->storage->pool->has_replicants
310     => 'does have replicants';     
311
312 is $replicated->schema->storage->pool->num_replicants => 2
313     => 'has two replicants';
314        
315 does_ok $replicated_storages[0]
316     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
317
318 does_ok $replicated_storages[1]
319     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
320     
321 does_ok $replicated->schema->storage->replicants->{$replicant_names[0]}
322     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
323
324 does_ok $replicated->schema->storage->replicants->{$replicant_names[1]}
325     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';  
326
327 ## Add some info to the database
328
329 $replicated
330     ->schema
331     ->populate('Artist', [
332         [ qw/artistid name/ ],
333         [ 4, "Ozric Tentacles"],
334     ]);
335                 
336 ## Make sure all the slaves have the table definitions
337
338 $replicated->replicate;
339 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(1);
340 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(1);
341
342 ## Silence warning about not supporting the is_replicating method if using the
343 ## sqlite dbs.
344 $replicated->schema->storage->debugobj->silence(1)
345   if first { m{^t/} } @replicant_names;
346  
347 $replicated->schema->storage->pool->validate_replicants;
348
349 $replicated->schema->storage->debugobj->silence(0);
350
351 ## Make sure we can read the data.
352
353 ok my $artist1 = $replicated->schema->resultset('Artist')->find(4)
354     => 'Created Result';
355
356 isa_ok $artist1
357     => 'DBICTest::Artist';
358     
359 is $artist1->name, 'Ozric Tentacles'
360     => 'Found expected name for first result';
361
362 ## Check that master_read_weight is honored
363 {
364     no warnings 'once';
365
366     # turn off redefined warning
367     local $SIG{__WARN__} = sub {};
368
369     local
370     *DBIx::Class::Storage::DBI::Replicated::Balancer::Random::_random_number =
371         sub { 999 };
372
373     $replicated->schema->storage->balancer->increment_storage;
374
375     is $replicated->schema->storage->balancer->current_replicant,
376        $replicated->schema->storage->master
377        => 'master_read_weight is honored';
378
379     ## turn it off for the duration of the test
380     $replicated->schema->storage->balancer->master_read_weight(0);
381     $replicated->schema->storage->balancer->increment_storage;
382 }
383
384 ## Add some new rows that only the master will have  This is because
385 ## we overload any type of write operation so that is must hit the master
386 ## database.
387
388 $replicated
389     ->schema
390     ->populate('Artist', [
391         [ qw/artistid name/ ],
392         [ 5, "Doom's Children"],
393         [ 6, "Dead On Arrival"],
394         [ 7, "Watergate"],
395     ]);
396
397 ## Make sure all the slaves have the table definitions
398 $replicated->replicate;
399
400 ## Should find some data now
401
402 ok my $artist2 = $replicated->schema->resultset('Artist')->find(5)
403     => 'Sync succeed';
404     
405 isa_ok $artist2
406     => 'DBICTest::Artist';
407     
408 is $artist2->name, "Doom's Children"
409     => 'Found expected name for first result';
410
411 ## What happens when we disconnect all the replicants?
412
413 is $replicated->schema->storage->pool->connected_replicants => 2
414     => "both replicants are connected";
415     
416 $replicated->schema->storage->replicants->{$replicant_names[0]}->disconnect;
417 $replicated->schema->storage->replicants->{$replicant_names[1]}->disconnect;
418
419 is $replicated->schema->storage->pool->connected_replicants => 0
420     => "both replicants are now disconnected";
421
422 ## All these should pass, since the database should automatically reconnect
423
424 ok my $artist3 = $replicated->schema->resultset('Artist')->find(6)
425     => 'Still finding stuff.';
426     
427 isa_ok $artist3
428     => 'DBICTest::Artist';
429     
430 is $artist3->name, "Dead On Arrival"
431     => 'Found expected name for first result';
432
433 is $replicated->schema->storage->pool->connected_replicants => 1
434     => "At Least One replicant reconnected to handle the job";
435     
436 ## What happens when we try to select something that doesn't exist?
437
438 ok ! $replicated->schema->resultset('Artist')->find(666)
439     => 'Correctly failed to find something.';
440     
441 ## test the reliable option
442
443 TESTRELIABLE: {
444         
445         $replicated->schema->storage->set_reliable_storage;
446         
447         ok $replicated->schema->resultset('Artist')->find(2)
448             => 'Read from master 1';
449         
450         ok $replicated->schema->resultset('Artist')->find(5)
451             => 'Read from master 2';
452             
453     $replicated->schema->storage->set_balanced_storage;     
454             
455         ok $replicated->schema->resultset('Artist')->find(3)
456         => 'Read from replicant';
457 }
458
459 ## Make sure when reliable goes out of scope, we are using replicants again
460
461 ok $replicated->schema->resultset('Artist')->find(1)
462     => 'back to replicant 1.';
463     
464 ok $replicated->schema->resultset('Artist')->find(2)
465     => 'back to replicant 2.';
466
467 ## set all the replicants to inactive, and make sure the balancer falls back to
468 ## the master.
469
470 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(0);
471 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(0);
472
473 {
474     ## catch the fallback to master warning
475     open my $debugfh, '>', \my $fallback_warning;
476     my $oldfh = $replicated->schema->storage->debugfh;
477     $replicated->schema->storage->debugfh($debugfh);
478
479     ok $replicated->schema->resultset('Artist')->find(2)
480         => 'Fallback to master';
481
482     like $fallback_warning, qr/falling back to master/
483         => 'emits falling back to master warning';
484
485     $replicated->schema->storage->debugfh($oldfh);
486 }
487
488 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(1);
489 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(1);
490
491 ## Silence warning about not supporting the is_replicating method if using the
492 ## sqlite dbs.
493 $replicated->schema->storage->debugobj->silence(1)
494   if first { m{^t/} } @replicant_names;
495  
496 $replicated->schema->storage->pool->validate_replicants;
497
498 $replicated->schema->storage->debugobj->silence(0);
499
500 ok $replicated->schema->resultset('Artist')->find(2)
501     => 'Returned to replicates';
502     
503 ## Getting slave status tests
504
505 SKIP: {
506     ## We skip this tests unless you have a custom replicants, since the default
507     ## sqlite based replication tests don't support these functions.
508     
509     skip 'Cannot Test Replicant Status on Non Replicating Database', 9
510      unless DBICTest->has_custom_dsn && $ENV{"DBICTEST_SLAVE0_DSN"};
511
512     $replicated->replicate; ## Give the slaves a chance to catchup.
513
514         ok $replicated->schema->storage->replicants->{$replicant_names[0]}->is_replicating
515             => 'Replicants are replicating';
516             
517         is $replicated->schema->storage->replicants->{$replicant_names[0]}->lag_behind_master, 0
518             => 'Replicant is zero seconds behind master';
519             
520         ## Test the validate replicants
521         
522         $replicated->schema->storage->pool->validate_replicants;
523         
524         is $replicated->schema->storage->pool->active_replicants, 2
525             => 'Still have 2 replicants after validation';
526             
527         ## Force the replicants to fail the validate test by required their lag to
528         ## be negative (ie ahead of the master!)
529         
530     $replicated->schema->storage->pool->maximum_lag(-10);
531     $replicated->schema->storage->pool->validate_replicants;
532     
533     is $replicated->schema->storage->pool->active_replicants, 0
534         => 'No way a replicant be be ahead of the master';
535         
536     ## Let's be fair to the replicants again.  Let them lag up to 5
537         
538     $replicated->schema->storage->pool->maximum_lag(5);
539     $replicated->schema->storage->pool->validate_replicants;
540     
541     is $replicated->schema->storage->pool->active_replicants, 2
542         => 'Both replicants in good standing again';    
543         
544         ## Check auto validate
545         
546         is $replicated->schema->storage->balancer->auto_validate_every, 100
547             => "Got the expected value for auto validate";
548             
549                 ## This will make sure we auto validatge everytime
550                 $replicated->schema->storage->balancer->auto_validate_every(0);
551                 
552                 ## set all the replicants to inactive, and make sure the balancer falls back to
553                 ## the master.
554                 
555                 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(0);
556                 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(0);
557                 
558                 ## Ok, now when we go to run a query, autovalidate SHOULD reconnect
559         
560         is $replicated->schema->storage->pool->active_replicants => 0
561             => "both replicants turned off";
562                 
563         ok $replicated->schema->resultset('Artist')->find(5)
564             => 'replicant reactivated';
565             
566         is $replicated->schema->storage->pool->active_replicants => 2
567             => "both replicants reactivated";        
568 }
569
570 ## Test the reliably callback
571
572 ok my $reliably = sub {
573         
574     ok $replicated->schema->resultset('Artist')->find(5)
575         => 'replicant reactivated';     
576         
577 } => 'created coderef properly';
578
579 $replicated->schema->storage->execute_reliably($reliably);
580
581 ## Try something with an error
582
583 ok my $unreliably = sub {
584     
585     ok $replicated->schema->resultset('ArtistXX')->find(5)
586         => 'replicant reactivated'; 
587     
588 } => 'created coderef properly';
589
590 throws_ok {$replicated->schema->storage->execute_reliably($unreliably)} 
591     qr/Can't find source for ArtistXX/
592     => 'Bad coderef throws proper error';
593     
594 ## Make sure replication came back
595
596 ok $replicated->schema->resultset('Artist')->find(3)
597     => 'replicant reactivated';
598     
599 ## make sure transactions are set to execute_reliably
600
601 ok my $transaction = sub {
602         
603         my $id = shift @_;
604         
605         $replicated
606             ->schema
607             ->populate('Artist', [
608                 [ qw/artistid name/ ],
609                 [ $id, "Children of the Grave"],
610             ]);
611             
612     ok my $result = $replicated->schema->resultset('Artist')->find($id)
613         => 'Found expected artist';
614         
615     ok my $more = $replicated->schema->resultset('Artist')->find(1)
616         => 'Found expected artist again';
617         
618    return ($result, $more);
619    
620 } => 'Created a coderef properly';
621
622 ## Test the transaction with multi return
623 {
624         ok my @return = $replicated->schema->txn_do($transaction, 666)
625             => 'did transaction';
626             
627             is $return[0]->id, 666
628                 => 'first returned value is correct';
629                 
630             is $return[1]->id, 1
631                 => 'second returned value is correct';
632 }
633
634 ## Test that asking for single return works
635 {
636         ok my $return = $replicated->schema->txn_do($transaction, 777)
637             => 'did transaction';
638             
639             is $return->id, 777
640                 => 'first returned value is correct';
641 }
642
643 ## Test transaction returning a single value
644
645 {
646         ok my $result = $replicated->schema->txn_do(sub {
647                 ok my $more = $replicated->schema->resultset('Artist')->find(1)
648                 => 'found inside a transaction';
649                 return $more;
650         }) => 'successfully processed transaction';
651         
652         is $result->id, 1
653            => 'Got expected single result from transaction';
654 }
655
656 ## Make sure replication came back
657
658 ok $replicated->schema->resultset('Artist')->find(1)
659     => 'replicant reactivated';
660     
661 ## Test Discard changes
662
663 {
664         ok my $artist = $replicated->schema->resultset('Artist')->find(2)
665             => 'got an artist to test discard changes';
666             
667         ok $artist->discard_changes
668            => 'properly discard changes';
669 }
670
671 ## Test some edge cases, like trying to do a transaction inside a transaction, etc
672
673 {
674     ok my $result = $replicated->schema->txn_do(sub {
675         return $replicated->schema->txn_do(sub {
676                 ok my $more = $replicated->schema->resultset('Artist')->find(1)
677                 => 'found inside a transaction inside a transaction';
678                 return $more;                   
679         });
680     }) => 'successfully processed transaction';
681     
682     is $result->id, 1
683        => 'Got expected single result from transaction';          
684 }
685
686 {
687     ok my $result = $replicated->schema->txn_do(sub {
688         return $replicated->schema->storage->execute_reliably(sub {
689                 return $replicated->schema->txn_do(sub {
690                         return $replicated->schema->storage->execute_reliably(sub {
691                                 ok my $more = $replicated->schema->resultset('Artist')->find(1)
692                                 => 'found inside crazy deep transactions and execute_reliably';
693                                 return $more;                           
694                         });
695                 });     
696         });
697     }) => 'successfully processed transaction';
698     
699     is $result->id, 1
700        => 'Got expected single result from transaction';          
701 }     
702
703 ## Test the force_pool resultset attribute.
704
705 {
706         ok my $artist_rs = $replicated->schema->resultset('Artist')
707         => 'got artist resultset';
708            
709         ## Turn on Forced Pool Storage
710         ok my $reliable_artist_rs = $artist_rs->search(undef, {force_pool=>'master'})
711         => 'Created a resultset using force_pool storage';
712            
713     ok my $artist = $reliable_artist_rs->find(2) 
714         => 'got an artist result via force_pool storage';
715 }
716
717 ## Delete the old database files
718 $replicated->cleanup;
719
720 # vim: sw=4 sts=4 :