a2463e4a625d209c29d19a7aba6fe75531fecf5c
[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 qw/once redefine/;
365
366     local
367     *DBIx::Class::Storage::DBI::Replicated::Balancer::Random::_random_number =
368         sub { 999 };
369
370     $replicated->schema->storage->balancer->increment_storage;
371
372     is $replicated->schema->storage->balancer->current_replicant,
373        $replicated->schema->storage->master
374        => 'master_read_weight is honored';
375
376     ## turn it off for the duration of the test
377     $replicated->schema->storage->balancer->master_read_weight(0);
378     $replicated->schema->storage->balancer->increment_storage;
379 }
380
381 ## Add some new rows that only the master will have  This is because
382 ## we overload any type of write operation so that is must hit the master
383 ## database.
384
385 $replicated
386     ->schema
387     ->populate('Artist', [
388         [ qw/artistid name/ ],
389         [ 5, "Doom's Children"],
390         [ 6, "Dead On Arrival"],
391         [ 7, "Watergate"],
392     ]);
393
394 ## Make sure all the slaves have the table definitions
395 $replicated->replicate;
396
397 ## Should find some data now
398
399 ok my $artist2 = $replicated->schema->resultset('Artist')->find(5)
400     => 'Sync succeed';
401     
402 isa_ok $artist2
403     => 'DBICTest::Artist';
404     
405 is $artist2->name, "Doom's Children"
406     => 'Found expected name for first result';
407
408 ## What happens when we disconnect all the replicants?
409
410 is $replicated->schema->storage->pool->connected_replicants => 2
411     => "both replicants are connected";
412     
413 $replicated->schema->storage->replicants->{$replicant_names[0]}->disconnect;
414 $replicated->schema->storage->replicants->{$replicant_names[1]}->disconnect;
415
416 is $replicated->schema->storage->pool->connected_replicants => 0
417     => "both replicants are now disconnected";
418
419 ## All these should pass, since the database should automatically reconnect
420
421 ok my $artist3 = $replicated->schema->resultset('Artist')->find(6)
422     => 'Still finding stuff.';
423     
424 isa_ok $artist3
425     => 'DBICTest::Artist';
426     
427 is $artist3->name, "Dead On Arrival"
428     => 'Found expected name for first result';
429
430 is $replicated->schema->storage->pool->connected_replicants => 1
431     => "At Least One replicant reconnected to handle the job";
432     
433 ## What happens when we try to select something that doesn't exist?
434
435 ok ! $replicated->schema->resultset('Artist')->find(666)
436     => 'Correctly failed to find something.';
437     
438 ## test the reliable option
439
440 TESTRELIABLE: {
441         
442         $replicated->schema->storage->set_reliable_storage;
443         
444         ok $replicated->schema->resultset('Artist')->find(2)
445             => 'Read from master 1';
446         
447         ok $replicated->schema->resultset('Artist')->find(5)
448             => 'Read from master 2';
449             
450     $replicated->schema->storage->set_balanced_storage;     
451             
452         ok $replicated->schema->resultset('Artist')->find(3)
453         => 'Read from replicant';
454 }
455
456 ## Make sure when reliable goes out of scope, we are using replicants again
457
458 ok $replicated->schema->resultset('Artist')->find(1)
459     => 'back to replicant 1.';
460     
461 ok $replicated->schema->resultset('Artist')->find(2)
462     => 'back to replicant 2.';
463
464 ## set all the replicants to inactive, and make sure the balancer falls back to
465 ## the master.
466
467 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(0);
468 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(0);
469
470 {
471     ## catch the fallback to master warning
472     open my $debugfh, '>', \my $fallback_warning;
473     my $oldfh = $replicated->schema->storage->debugfh;
474     $replicated->schema->storage->debugfh($debugfh);
475
476     ok $replicated->schema->resultset('Artist')->find(2)
477         => 'Fallback to master';
478
479     like $fallback_warning, qr/falling back to master/
480         => 'emits falling back to master warning';
481
482     $replicated->schema->storage->debugfh($oldfh);
483 }
484
485 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(1);
486 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(1);
487
488 ## Silence warning about not supporting the is_replicating method if using the
489 ## sqlite dbs.
490 $replicated->schema->storage->debugobj->silence(1)
491   if first { m{^t/} } @replicant_names;
492  
493 $replicated->schema->storage->pool->validate_replicants;
494
495 $replicated->schema->storage->debugobj->silence(0);
496
497 ok $replicated->schema->resultset('Artist')->find(2)
498     => 'Returned to replicates';
499     
500 ## Getting slave status tests
501
502 SKIP: {
503     ## We skip this tests unless you have a custom replicants, since the default
504     ## sqlite based replication tests don't support these functions.
505     
506     skip 'Cannot Test Replicant Status on Non Replicating Database', 9
507      unless DBICTest->has_custom_dsn && $ENV{"DBICTEST_SLAVE0_DSN"};
508
509     $replicated->replicate; ## Give the slaves a chance to catchup.
510
511         ok $replicated->schema->storage->replicants->{$replicant_names[0]}->is_replicating
512             => 'Replicants are replicating';
513             
514         is $replicated->schema->storage->replicants->{$replicant_names[0]}->lag_behind_master, 0
515             => 'Replicant is zero seconds behind master';
516             
517         ## Test the validate replicants
518         
519         $replicated->schema->storage->pool->validate_replicants;
520         
521         is $replicated->schema->storage->pool->active_replicants, 2
522             => 'Still have 2 replicants after validation';
523             
524         ## Force the replicants to fail the validate test by required their lag to
525         ## be negative (ie ahead of the master!)
526         
527     $replicated->schema->storage->pool->maximum_lag(-10);
528     $replicated->schema->storage->pool->validate_replicants;
529     
530     is $replicated->schema->storage->pool->active_replicants, 0
531         => 'No way a replicant be be ahead of the master';
532         
533     ## Let's be fair to the replicants again.  Let them lag up to 5
534         
535     $replicated->schema->storage->pool->maximum_lag(5);
536     $replicated->schema->storage->pool->validate_replicants;
537     
538     is $replicated->schema->storage->pool->active_replicants, 2
539         => 'Both replicants in good standing again';    
540         
541         ## Check auto validate
542         
543         is $replicated->schema->storage->balancer->auto_validate_every, 100
544             => "Got the expected value for auto validate";
545             
546                 ## This will make sure we auto validatge everytime
547                 $replicated->schema->storage->balancer->auto_validate_every(0);
548                 
549                 ## set all the replicants to inactive, and make sure the balancer falls back to
550                 ## the master.
551                 
552                 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(0);
553                 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(0);
554                 
555                 ## Ok, now when we go to run a query, autovalidate SHOULD reconnect
556         
557         is $replicated->schema->storage->pool->active_replicants => 0
558             => "both replicants turned off";
559                 
560         ok $replicated->schema->resultset('Artist')->find(5)
561             => 'replicant reactivated';
562             
563         is $replicated->schema->storage->pool->active_replicants => 2
564             => "both replicants reactivated";        
565 }
566
567 ## Test the reliably callback
568
569 ok my $reliably = sub {
570         
571     ok $replicated->schema->resultset('Artist')->find(5)
572         => 'replicant reactivated';     
573         
574 } => 'created coderef properly';
575
576 $replicated->schema->storage->execute_reliably($reliably);
577
578 ## Try something with an error
579
580 ok my $unreliably = sub {
581     
582     ok $replicated->schema->resultset('ArtistXX')->find(5)
583         => 'replicant reactivated'; 
584     
585 } => 'created coderef properly';
586
587 throws_ok {$replicated->schema->storage->execute_reliably($unreliably)} 
588     qr/Can't find source for ArtistXX/
589     => 'Bad coderef throws proper error';
590     
591 ## Make sure replication came back
592
593 ok $replicated->schema->resultset('Artist')->find(3)
594     => 'replicant reactivated';
595     
596 ## make sure transactions are set to execute_reliably
597
598 ok my $transaction = sub {
599         
600         my $id = shift @_;
601         
602         $replicated
603             ->schema
604             ->populate('Artist', [
605                 [ qw/artistid name/ ],
606                 [ $id, "Children of the Grave"],
607             ]);
608             
609     ok my $result = $replicated->schema->resultset('Artist')->find($id)
610         => 'Found expected artist';
611         
612     ok my $more = $replicated->schema->resultset('Artist')->find(1)
613         => 'Found expected artist again';
614         
615    return ($result, $more);
616    
617 } => 'Created a coderef properly';
618
619 ## Test the transaction with multi return
620 {
621         ok my @return = $replicated->schema->txn_do($transaction, 666)
622             => 'did transaction';
623             
624             is $return[0]->id, 666
625                 => 'first returned value is correct';
626                 
627             is $return[1]->id, 1
628                 => 'second returned value is correct';
629 }
630
631 ## Test that asking for single return works
632 {
633         ok my $return = $replicated->schema->txn_do($transaction, 777)
634             => 'did transaction';
635             
636             is $return->id, 777
637                 => 'first returned value is correct';
638 }
639
640 ## Test transaction returning a single value
641
642 {
643         ok my $result = $replicated->schema->txn_do(sub {
644                 ok my $more = $replicated->schema->resultset('Artist')->find(1)
645                 => 'found inside a transaction';
646                 return $more;
647         }) => 'successfully processed transaction';
648         
649         is $result->id, 1
650            => 'Got expected single result from transaction';
651 }
652
653 ## Make sure replication came back
654
655 ok $replicated->schema->resultset('Artist')->find(1)
656     => 'replicant reactivated';
657     
658 ## Test Discard changes
659
660 {
661         ok my $artist = $replicated->schema->resultset('Artist')->find(2)
662             => 'got an artist to test discard changes';
663             
664         ok $artist->discard_changes
665            => 'properly discard changes';
666 }
667
668 ## Test some edge cases, like trying to do a transaction inside a transaction, etc
669
670 {
671     ok my $result = $replicated->schema->txn_do(sub {
672         return $replicated->schema->txn_do(sub {
673                 ok my $more = $replicated->schema->resultset('Artist')->find(1)
674                 => 'found inside a transaction inside a transaction';
675                 return $more;                   
676         });
677     }) => 'successfully processed transaction';
678     
679     is $result->id, 1
680        => 'Got expected single result from transaction';          
681 }
682
683 {
684     ok my $result = $replicated->schema->txn_do(sub {
685         return $replicated->schema->storage->execute_reliably(sub {
686                 return $replicated->schema->txn_do(sub {
687                         return $replicated->schema->storage->execute_reliably(sub {
688                                 ok my $more = $replicated->schema->resultset('Artist')->find(1)
689                                 => 'found inside crazy deep transactions and execute_reliably';
690                                 return $more;                           
691                         });
692                 });     
693         });
694     }) => 'successfully processed transaction';
695     
696     is $result->id, 1
697        => 'Got expected single result from transaction';          
698 }     
699
700 ## Test the force_pool resultset attribute.
701
702 {
703         ok my $artist_rs = $replicated->schema->resultset('Artist')
704         => 'got artist resultset';
705            
706         ## Turn on Forced Pool Storage
707         ok my $reliable_artist_rs = $artist_rs->search(undef, {force_pool=>'master'})
708         => 'Created a resultset using force_pool storage';
709            
710     ok my $artist = $reliable_artist_rs->find(2) 
711         => 'got an artist result via force_pool storage';
712 }
713
714 ## Delete the old database files
715 $replicated->cleanup;
716
717 # vim: sw=4 sts=4 :