got first pass on the replication and balancer, passing all of the old test suite...
[dbsrgits/DBIx-Class.git] / t / 93storage_replication.t
CommitLineData
e4dc89b3 1use strict;
2use warnings;
3use lib qw(t/lib);
e4dc89b3 4use Test::More;
2bf79155 5use Data::Dump qw/dump/;
8f7986d6 6
86583fa7 7BEGIN {
2bf79155 8 eval "use Moose";
86583fa7 9 plan $@
2bf79155 10 ? ( skip_all => 'needs Moose for testing' )
26ab719a 11 : ( tests => 30 );
12}
13
14use_ok 'DBIx::Class::Storage::DBI::Replicated::Pool';
15use_ok 'DBIx::Class::Storage::DBI::Replicated::Balancer';
16use_ok 'DBIx::Class::Storage::DBI::Replicated::Replicant';
17use_ok 'DBIx::Class::Storage::DBI::Replicated';
0f83441a 18
19## ----------------------------------------------------------------------------
20## Build a class to hold all our required testing data and methods.
21## ----------------------------------------------------------------------------
22
23TESTSCHEMACLASS: {
2bf79155 24
25 package DBIx::Class::DBI::Replicated::TestReplication;
26
27 use DBICTest;
26ab719a 28 use File::Copy;
29
2bf79155 30 use base qw/Class::Accessor::Fast/;
31
26ab719a 32 __PACKAGE__->mk_accessors( qw/schema master_path slave_paths/ );
2bf79155 33
34 ## Initialize the object
35
36 sub new {
26ab719a 37 my $class = shift @_;
38 my $self = $class->SUPER::new(@_);
2bf79155 39
40 $self->schema( $self->init_schema );
26ab719a 41 $self->master_path("t/var/DBIxClass.db");
2bf79155 42
43 return $self;
44 }
45
26ab719a 46 ## Get the Schema and set the replication storage type
2bf79155 47
48 sub init_schema {
49 my $class = shift @_;
50 my $schema = DBICTest->init_schema(storage_type=>'::DBI::Replicated');
51 return $schema;
52 }
26ab719a 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 }
2bf79155 84}
85
86## ----------------------------------------------------------------------------
87## Create an object and run some tests
88## ----------------------------------------------------------------------------
89
26ab719a 90## Thi first bunch of tests are basic, just make sure all the bits are behaving
2bf79155 91
26ab719a 92ok 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';
2bf79155 99
26ab719a 100isa_ok $replicated->schema
2bf79155 101 => 'DBIx::Class::Schema';
102
26ab719a 103isa_ok $replicated->schema->storage
104 => 'DBIx::Class::Storage::DBI::Replicated';
105
106ok $replicated->schema->storage->meta
107 => 'has a meta object';
108
109isa_ok $replicated->schema->storage->master
110 => 'DBIx::Class::Storage::DBI';
111
112isa_ok $replicated->schema->storage->pool
113 => 'DBIx::Class::Storage::DBI::Replicated::Pool';
114
115isa_ok $replicated->schema->storage->balancer
116 => 'DBIx::Class::Storage::DBI::Replicated::Balancer';
117
118ok my @replicant_connects = $replicated->generate_replicant_connect_info
119 => 'got replication connect information';
120
121ok my @replicated_storages = $replicated->schema->storage->create_replicants(@replicant_connects)
122 => 'Created some storages suitable for replicants';
123
124isa_ok $replicated->schema->storage->current_replicant
125 => 'DBIx::Class::Storage::DBI';
126
127ok $replicated->schema->storage->pool->has_replicants
128 => 'does have replicants';
129
130is $replicated->schema->storage->num_replicants => 2
131 => 'has two replicants';
132
133isa_ok $replicated_storages[0]
134 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
135
136isa_ok $replicated_storages[1]
137 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
138
139isa_ok $replicated->schema->storage->replicants->{"t/var/DBIxClass_slave1.db"}
140 => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
141
142isa_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
160ok my $artist1 = $replicated->schema->resultset('Artist')->find(4)
161 => 'Created Result';
162
163isa_ok $artist1
164 => 'DBICTest::Artist';
165
166is $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
185is $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
193ok my $artist2 = $replicated->schema->resultset('Artist')->find(5)
194 => 'Sync succeed';
195
196isa_ok $artist2
197 => 'DBICTest::Artist';
198
199is $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
207ok my $artist3 = $replicated->schema->resultset('Artist')->find(6)
208 => 'Still finding stuff.';
2bf79155 209
26ab719a 210isa_ok $artist3
211 => 'DBICTest::Artist';
2bf79155 212
26ab719a 213is $artist3->name, "Dead On Arrival"
214 => 'Found expected name for first result';
2bf79155 215
216
217__END__
218
219## ----------------------------------------------------------------------------
220## Build a class to hold all our required testing data and methods.
221## ----------------------------------------------------------------------------
222
223TESTSCHEMACLASS: {
0f83441a 224
2156bbdd 225 package DBIx::Class::DBI::Replicated::TestReplication;
0f83441a 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();
2156bbdd 266 $schema->storage_type( '::DBI::Replicated' );
0f83441a 267
268 return $schema;
269 }
270
271 ## connect the Schema
272
273 sub connect {
274 my $self = shift @_;
275 my ($master, @slaves) = @{$self->{dsns}};
2156bbdd 276 my $master_connect_info = [$master, '','', {AutoCommit=>1, PrintError=>0}];
0f83441a 277
2156bbdd 278 my @slavesob;
0f83441a 279 foreach my $slave (@slaves)
280 {
281 my $dbh = shift @{$self->{slaves}}
282 || DBI->connect($slave,"","",{PrintError=>0, PrintWarn=>0});
283
2156bbdd 284 push @{$master_connect_info->[-1]->{slaves_connect_info}},
0f83441a 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}
2156bbdd 296 ->connect(@$master_connect_info);
0f83441a 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 }
86583fa7 329}
e4dc89b3 330
0f83441a 331## ----------------------------------------------------------------------------
332## Create an object and run some tests
333## ----------------------------------------------------------------------------
334
335my %params = (
336 db_paths => [
337 "t/var/DBIxClass.db",
338 "t/var/DBIxClass_slave1.db",
339 "t/var/DBIxClass_slave2.db",
340 ],
341);
342
2156bbdd 343ok my $replicate = DBIx::Class::DBI::Replicated::TestReplication->new(%params)
0f83441a 344 => 'Created a replication object';
345
346isa_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
364ok my $artist1 = $replicate->{schema}->resultset('Artist')->find(4)
365 => 'Created Result';
366
367isa_ok $artist1
368 => 'DBICTest::Artist';
369
370is $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
0f83441a 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
392is $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
400ok my $artist2 = $replicate->{schema}->resultset('Artist')->find(5)
401 => 'Sync succeed';
402
403isa_ok $artist2
404 => 'DBICTest::Artist';
405
406is $artist2->name, "Doom's Children"
407 => 'Found expected name for first result';
408
409## What happens when we delete one of the slaves?
410
411ok my $slave1 = @{$replicate->{slaves}}[0]
412 => 'Got Slave1';
413
414ok $slave1->disconnect
415 => 'disconnected slave1';
416
417$replicate->reconnect;
418
419ok my $artist3 = $replicate->{schema}->resultset('Artist')->find(6)
420 => 'Still finding stuff.';
421
422isa_ok $artist3
423 => 'DBICTest::Artist';
424
425is $artist3->name, "Dead On Arrival"
426 => 'Found expected name for first result';
427
428## Let's delete all the slaves
429
430ok my $slave2 = @{$replicate->{slaves}}[1]
431 => 'Got Slave2';
432
433ok $slave2->disconnect
434 => 'Disconnected slave2';
435
436$replicate->reconnect;
437
438## We expect an error now, since all the slaves are dead
439
440eval {
441 $replicate->{schema}->resultset('Artist')->find(4)->name;
442};
443
444ok $@ => 'Got error when trying to find artistid 4';
445
446## This should also be an error
447
448eval {
449 my $artist4 = $replicate->{schema}->resultset('Artist')->find(7);
450};
451
452ok $@ => 'Got read errors after everything failed';
453
2156bbdd 454## make sure ->connect_info returns something sane
455
456ok $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
465ok ! $replicate->{schema}->resultset('Artist')->find(666)
466 => 'Correctly did not find a bad artist id';
467
0f83441a 468## Delete the old database files
469$replicate->cleanup;
470
471
472
473
474
475