Commit | Line | Data |
e4dc89b3 |
1 | use strict; |
2 | use warnings; |
3 | use lib qw(t/lib); |
e4dc89b3 |
4 | use Test::More; |
2bf79155 |
5 | use Data::Dump qw/dump/; |
8f7986d6 |
6 | |
86583fa7 |
7 | BEGIN { |
2bf79155 |
8 | eval "use Moose"; |
86583fa7 |
9 | plan $@ |
2bf79155 |
10 | ? ( skip_all => 'needs Moose for testing' ) |
26ab719a |
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'; |
0f83441a |
18 | |
19 | ## ---------------------------------------------------------------------------- |
20 | ## Build a class to hold all our required testing data and methods. |
21 | ## ---------------------------------------------------------------------------- |
22 | |
23 | TESTSCHEMACLASS: { |
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 |
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'; |
2bf79155 |
99 | |
26ab719a |
100 | isa_ok $replicated->schema |
2bf79155 |
101 | => 'DBIx::Class::Schema'; |
102 | |
26ab719a |
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.'; |
2bf79155 |
209 | |
26ab719a |
210 | isa_ok $artist3 |
211 | => 'DBICTest::Artist'; |
2bf79155 |
212 | |
26ab719a |
213 | is $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 | |
223 | TESTSCHEMACLASS: { |
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 | |
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 | |
2156bbdd |
343 | ok my $replicate = DBIx::Class::DBI::Replicated::TestReplication->new(%params) |
0f83441a |
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 | |
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 | |
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 | |
2156bbdd |
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 | |
0f83441a |
468 | ## Delete the old database files |
469 | $replicate->cleanup; |
470 | |
471 | |
472 | |
473 | |
474 | |
475 | |