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