got first pass on the replication and balancer, passing all of the old test suite...
[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 Data::Dump qw/dump/;
6
7 BEGIN {
8     eval "use Moose";
9     plan $@
10         ? ( skip_all => 'needs Moose for testing' )
11         : ( tests => 30 );
12 }
13
14 use_ok 'DBIx::Class::Storage::DBI::Replicated::Pool';
15 use_ok 'DBIx::Class::Storage::DBI::Replicated::Balancer';
16 use_ok 'DBIx::Class::Storage::DBI::Replicated::Replicant';
17 use_ok 'DBIx::Class::Storage::DBI::Replicated';
18
19 ## ----------------------------------------------------------------------------
20 ## Build a class to hold all our required testing data and methods.
21 ## ----------------------------------------------------------------------------
22
23 TESTSCHEMACLASS: {
24
25     package DBIx::Class::DBI::Replicated::TestReplication;
26    
27     use DBICTest;
28     use File::Copy;
29     
30     use base qw/Class::Accessor::Fast/;
31     
32     __PACKAGE__->mk_accessors( qw/schema master_path slave_paths/ );
33
34     ## Initialize the object
35     
36         sub new {
37             my $class = shift @_;
38             my $self = $class->SUPER::new(@_);
39         
40             $self->schema( $self->init_schema );
41             $self->master_path("t/var/DBIxClass.db");
42         
43             return $self;
44         }
45     
46     ## Get the Schema and set the replication storage type
47     
48     sub init_schema {
49         my $class = shift @_;
50         my $schema = DBICTest->init_schema(storage_type=>'::DBI::Replicated');
51         return $schema;
52     }
53     
54     ## Return an Array of ArrayRefs where each ArrayRef is suitable to use for
55     ## $storage->connect_info to be used for connecting replicants.
56     
57     sub generate_replicant_connect_info {
58         my $self = shift @_;
59         my @dsn = map {
60             "dbi:SQLite:${_}";
61         } @{$self->slave_paths};
62         
63         return map { [$_,'','',{}] } @dsn;
64     }
65     
66     ## Do a 'good enough' replication by copying the master dbfile over each of
67     ## the slave dbfiles.
68     
69     sub replicate {
70         my $self = shift @_;
71         foreach my $slave (@{$self->slave_paths}) {
72             copy($self->master_path, $slave);
73         }
74     }
75     
76     ## Cleanup after ourselves.  Unlink all gthe slave paths.
77     
78     sub cleanup {
79         my $self = shift @_;
80         foreach my $slave (@{$self->slave_paths}) {
81             unlink $slave;
82         }     
83     }
84 }
85
86 ## ----------------------------------------------------------------------------
87 ## Create an object and run some tests
88 ## ----------------------------------------------------------------------------
89
90 ## Thi first bunch of tests are basic, just make sure all the bits are behaving
91
92 ok my $replicated = DBIx::Class::DBI::Replicated::TestReplication
93     ->new({
94         slave_paths=>[
95                 "t/var/DBIxClass_slave1.db",
96                 "t/var/DBIxClass_slave2.db",    
97         ],
98     }) => 'Created a replication object';
99     
100 isa_ok $replicated->schema
101     => 'DBIx::Class::Schema';
102     
103 isa_ok $replicated->schema->storage
104     => 'DBIx::Class::Storage::DBI::Replicated';
105
106 ok $replicated->schema->storage->meta
107     => 'has a meta object';
108     
109 isa_ok $replicated->schema->storage->master
110     => 'DBIx::Class::Storage::DBI';
111     
112 isa_ok $replicated->schema->storage->pool
113     => 'DBIx::Class::Storage::DBI::Replicated::Pool';
114     
115 isa_ok $replicated->schema->storage->balancer
116     => 'DBIx::Class::Storage::DBI::Replicated::Balancer'; 
117
118 ok my @replicant_connects = $replicated->generate_replicant_connect_info
119     => 'got replication connect information';
120
121 ok my @replicated_storages = $replicated->schema->storage->create_replicants(@replicant_connects)
122     => 'Created some storages suitable for replicants';
123     
124 isa_ok $replicated->schema->storage->current_replicant
125     => 'DBIx::Class::Storage::DBI';
126     
127 ok $replicated->schema->storage->pool->has_replicants
128     => 'does have replicants';     
129
130 is $replicated->schema->storage->num_replicants => 2
131     => 'has two replicants';
132        
133 isa_ok $replicated_storages[0]
134     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
135
136 isa_ok $replicated_storages[1]
137     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
138     
139 isa_ok $replicated->schema->storage->replicants->{"t/var/DBIxClass_slave1.db"}
140     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
141
142 isa_ok $replicated->schema->storage->replicants->{"t/var/DBIxClass_slave2.db"}
143     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';  
144
145 ## Add some info to the database
146
147 $replicated
148     ->schema
149     ->populate('Artist', [
150         [ qw/artistid name/ ],
151         [ 4, "Ozric Tentacles"],
152     ]);
153                 
154 ## Make sure all the slaves have the table definitions
155
156 $replicated->replicate;
157
158 ## Make sure we can read the data.
159
160 ok my $artist1 = $replicated->schema->resultset('Artist')->find(4)
161     => 'Created Result';
162
163 isa_ok $artist1
164     => 'DBICTest::Artist';
165     
166 is $artist1->name, 'Ozric Tentacles'
167     => 'Found expected name for first result';
168
169 ## Add some new rows that only the master will have  This is because
170 ## we overload any type of write operation so that is must hit the master
171 ## database.
172
173 $replicated
174     ->schema
175     ->populate('Artist', [
176         [ qw/artistid name/ ],
177         [ 5, "Doom's Children"],
178         [ 6, "Dead On Arrival"],
179         [ 7, "Watergate"],
180     ]);
181
182 ## Alright, the database 'cluster' is not in a consistent state.  When we do
183 ## a read now we expect bad news
184
185 is $replicated->schema->resultset('Artist')->find(5), undef
186     => 'read after disconnect fails because it uses a replicant which we have neglected to "replicate" yet';
187
188 ## Make sure all the slaves have the table definitions
189 $replicated->replicate;
190
191 ## Should find some data now
192
193 ok my $artist2 = $replicated->schema->resultset('Artist')->find(5)
194     => 'Sync succeed';
195     
196 isa_ok $artist2
197     => 'DBICTest::Artist';
198     
199 is $artist2->name, "Doom's Children"
200     => 'Found expected name for first result';
201
202 ## What happens when we disconnect all the replicants?
203
204 $replicated->schema->storage->replicants->{"t/var/DBIxClass_slave1.db"}->disconnect;
205 $replicated->schema->storage->replicants->{"t/var/DBIxClass_slave2.db"}->disconnect;
206
207 ok my $artist3 = $replicated->schema->resultset('Artist')->find(6)
208     => 'Still finding stuff.';
209     
210 isa_ok $artist3
211     => 'DBICTest::Artist';
212     
213 is $artist3->name, "Dead On Arrival"
214     => 'Found expected name for first result';
215
216
217 __END__
218
219 ## ----------------------------------------------------------------------------
220 ## Build a class to hold all our required testing data and methods.
221 ## ----------------------------------------------------------------------------
222
223 TESTSCHEMACLASS: {
224         
225         package DBIx::Class::DBI::Replicated::TestReplication;
226
227         use DBI;        
228         use DBICTest;
229         use File::Copy;
230         
231         ## Create a constructor
232         
233         sub new {
234                 my $class = shift @_;
235                 my %params = @_;
236                 
237                 my $self = bless {
238                         db_paths => $params{db_paths},
239                         dsns => $class->init_dsns(%params),
240                         schema=>$class->init_schema,
241                 }, $class;
242                 
243                 $self->connect;
244                 return $self;
245         }
246         
247         ## get the DSNs.  We build this up from the list of file paths
248         
249         sub init_dsns {
250                 my $class = shift @_;
251                 my %params = @_;
252                 my $db_paths = $params{db_paths};
253
254                 my @dsn = map {
255                         "dbi:SQLite:${_}";
256                 } @$db_paths;
257                 
258                 return \@dsn;
259         }
260
261         ## get the Schema and set the replication storage type
262         
263         sub init_schema {
264                 my $class = shift @_;
265                 my $schema = DBICTest->init_schema();
266                 $schema->storage_type( '::DBI::Replicated' );
267                 
268                 return $schema;
269         }
270         
271         ## connect the Schema
272         
273         sub connect {
274                 my $self = shift @_;
275                 my ($master, @slaves) = @{$self->{dsns}};
276                 my $master_connect_info = [$master, '','', {AutoCommit=>1, PrintError=>0}];
277                 
278                 my @slavesob;
279                 foreach my $slave (@slaves)
280                 {
281                         my $dbh = shift @{$self->{slaves}}
282                          || DBI->connect($slave,"","",{PrintError=>0, PrintWarn=>0});
283                         
284                         push @{$master_connect_info->[-1]->{slaves_connect_info}},
285                          [$dbh, '','',{priority=>10}];
286                          
287                         push @slavesob,
288                          $dbh;
289                 }
290                 
291                 ## Keep track of the created slave databases
292                 $self->{slaves} = \@slavesob;
293                 
294                 $self
295                         ->{schema}
296                         ->connect(@$master_connect_info);
297         }
298         
299         ## replication
300         
301         sub replicate {
302                 my $self = shift @_;
303                 my ($master, @slaves) = @{$self->{db_paths}};
304                 
305                 foreach my $slave (@slaves) {
306                         copy($master, $slave);
307                 }
308         }
309         
310         ## Cleanup afer ourselves.
311         
312         sub cleanup {
313                 my $self = shift @_;
314                 my ($master, @slaves) = @{$self->{db_paths}};
315                 
316                 foreach my $slave (@slaves) {
317                         unlink $slave;
318                 }               
319         }
320         
321         ## Force a reconnection
322         
323         sub reconnect {
324                 my $self = shift @_;
325                 my $schema = $self->connect;
326                 $self->{schema} = $schema;
327                 return $schema;
328         }
329 }
330
331 ## ----------------------------------------------------------------------------
332 ## Create an object and run some tests
333 ## ----------------------------------------------------------------------------
334
335 my %params = (
336         db_paths => [
337                 "t/var/DBIxClass.db",
338                 "t/var/DBIxClass_slave1.db",
339                 "t/var/DBIxClass_slave2.db",
340         ],
341 );
342
343 ok my $replicate = DBIx::Class::DBI::Replicated::TestReplication->new(%params)
344         => 'Created a replication object';
345         
346 isa_ok $replicate->{schema}
347         => 'DBIx::Class::Schema';
348
349 ## Add some info to the database
350
351 $replicate
352         ->{schema}
353         ->populate('Artist', [
354                 [ qw/artistid name/ ],
355                 [ 4, "Ozric Tentacles"],
356         ]);
357                             
358 ## Make sure all the slaves have the table definitions
359
360 $replicate->replicate;
361
362 ## Make sure we can read the data.
363
364 ok my $artist1 = $replicate->{schema}->resultset('Artist')->find(4)
365         => 'Created Result';
366
367 isa_ok $artist1
368         => 'DBICTest::Artist';
369         
370 is $artist1->name, 'Ozric Tentacles'
371         => 'Found expected name for first result';
372
373 ## Add some new rows that only the master will have  This is because
374 ## we overload any type of write operation so that is must hit the master
375 ## database.
376
377 $replicate
378         ->{schema}
379         ->populate('Artist', [
380                 [ qw/artistid name/ ],
381                 [ 5, "Doom's Children"],
382                 [ 6, "Dead On Arrival"],
383                 [ 7, "Watergate"],
384         ]);
385         
386 ## Reconnect the database
387 $replicate->reconnect;
388
389 ## Alright, the database 'cluster' is not in a consistent state.  When we do
390 ## a read now we expect bad news
391
392 is $replicate->{schema}->resultset('Artist')->find(5), undef
393         => 'read after disconnect fails because it uses slave 1 which we have neglected to "replicate" yet';
394
395 ## Make sure all the slaves have the table definitions
396 $replicate->replicate;
397
398 ## Should find some data now
399
400 ok my $artist2 = $replicate->{schema}->resultset('Artist')->find(5)
401         => 'Sync succeed';
402         
403 isa_ok $artist2
404         => 'DBICTest::Artist';
405         
406 is $artist2->name, "Doom's Children"
407         => 'Found expected name for first result';
408         
409 ## What happens when we delete one of the slaves?
410
411 ok my $slave1 = @{$replicate->{slaves}}[0]
412         => 'Got Slave1';
413
414 ok $slave1->disconnect
415         => 'disconnected slave1';
416
417 $replicate->reconnect;
418
419 ok my $artist3 = $replicate->{schema}->resultset('Artist')->find(6)
420         => 'Still finding stuff.';
421         
422 isa_ok $artist3
423         => 'DBICTest::Artist';
424         
425 is $artist3->name, "Dead On Arrival"
426         => 'Found expected name for first result';
427         
428 ## Let's delete all the slaves
429
430 ok my $slave2 = @{$replicate->{slaves}}[1]
431         => 'Got Slave2';
432
433 ok $slave2->disconnect
434         => 'Disconnected slave2';
435
436 $replicate->reconnect;
437
438 ## We expect an error now, since all the slaves are dead
439
440 eval {
441         $replicate->{schema}->resultset('Artist')->find(4)->name;
442 };
443
444 ok $@ => 'Got error when trying to find artistid 4';
445
446 ## This should also be an error
447
448 eval {
449         my $artist4 = $replicate->{schema}->resultset('Artist')->find(7);       
450 };
451
452 ok $@ => 'Got read errors after everything failed';
453
454 ## make sure ->connect_info returns something sane
455
456 ok $replicate->{schema}->storage->connect_info
457     => 'got something out of ->connect_info';
458
459 ## Force a connection to the write source for testing.
460
461 $replicate->{schema}->storage($replicate->{schema}->storage->write_source);
462
463 ## What happens when we do a find for something that doesn't exist?
464
465 ok ! $replicate->{schema}->resultset('Artist')->find(666)
466     => 'Correctly did not find a bad artist id';
467
468 ## Delete the old database files
469 $replicate->cleanup;
470
471
472
473
474
475