Merge 'trunk' into 'replication_dedux'
[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 DBICTest;
6
7 BEGIN {
8     eval "use Moose; use Test::Moose";
9     plan $@
10         ? ( skip_all => 'needs Moose for testing' )
11         : ( tests => 46 );
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 =head1 HOW TO USE
20
21     This is a test of the replicated storage system.  This will work in one of
22     two ways, either it was try to fake replication with a couple of SQLite DBs
23     and creative use of copy, or if you define a couple of %ENV vars correctly
24     will try to test those.  If you do that, it will assume the setup is properly
25     replicating.  Your results may vary, but I have demonstrated this to work with
26     mysql native replication.
27     
28 =cut
29
30
31 ## ----------------------------------------------------------------------------
32 ## Build a class to hold all our required testing data and methods.
33 ## ----------------------------------------------------------------------------
34
35 TESTSCHEMACLASSES: {
36
37     ## --------------------------------------------------------------------- ##
38     ## Create an object to contain your replicated stuff.
39     ## --------------------------------------------------------------------- ##
40     
41     package DBIx::Class::DBI::Replicated::TestReplication;
42    
43     use DBICTest;
44     use base qw/Class::Accessor::Fast/;
45     
46     __PACKAGE__->mk_accessors( qw/schema/ );
47
48     ## Initialize the object
49     
50         sub new {
51             my $class = shift @_;
52             my $self = $class->SUPER::new(@_);
53         
54             $self->schema( $self->init_schema );
55             return $self;
56         }
57     
58     ## Get the Schema and set the replication storage type
59     
60     sub init_schema {
61         my $class = shift @_;
62         
63         my $schema = DBICTest->init_schema(
64             storage_type=>[
65                 '::DBI::Replicated' => {
66                         balancer_type=>'::Random',
67                 }
68             ],
69             deploy_args=>{
70                    add_drop_table => 1,
71             },
72         );
73
74         return $schema;
75     }
76     
77     sub generate_replicant_connect_info {}
78     sub replicate {}
79     sub cleanup {}
80
81   
82     ## --------------------------------------------------------------------- ##
83     ## Subclass for when you are using SQLite for testing, this provides a fake
84     ## replication support.
85     ## --------------------------------------------------------------------- ##
86         
87     package DBIx::Class::DBI::Replicated::TestReplication::SQLite;
88
89     use DBICTest;
90     use File::Copy;    
91     use base 'DBIx::Class::DBI::Replicated::TestReplication';
92     
93     __PACKAGE__->mk_accessors( qw/master_path slave_paths/ );
94     
95     ## Set the mastep path from DBICTest
96     
97         sub new {
98             my $class = shift @_;
99             my $self = $class->SUPER::new(@_);
100         
101             $self->master_path( DBICTest->_sqlite_dbfilename );
102             $self->slave_paths([
103             "t/var/DBIxClass_slave1.db",
104             "t/var/DBIxClass_slave2.db",    
105         ]);
106         
107             return $self;
108         }    
109         
110     ## Return an Array of ArrayRefs where each ArrayRef is suitable to use for
111     ## $storage->connect_info to be used for connecting replicants.
112     
113     sub generate_replicant_connect_info {
114         my $self = shift @_;
115         my @dsn = map {
116             "dbi:SQLite:${_}";
117         } @{$self->slave_paths};
118         
119         return map { [$_,'','',{AutoCommit=>1}] } @dsn;
120     }
121     
122     ## Do a 'good enough' replication by copying the master dbfile over each of
123     ## the slave dbfiles.  If the master is SQLite we do this, otherwise we
124     ## just do a one second pause to let the slaves catch up.
125     
126     sub replicate {
127         my $self = shift @_;
128         foreach my $slave (@{$self->slave_paths}) {
129             copy($self->master_path, $slave);
130         }
131     }
132     
133     ## Cleanup after ourselves.  Unlink all gthe slave paths.
134     
135     sub cleanup {
136         my $self = shift @_;
137         foreach my $slave (@{$self->slave_paths}) {
138             unlink $slave;
139         }     
140     }
141     
142     ## --------------------------------------------------------------------- ##
143     ## Subclass for when you are setting the databases via custom export vars
144     ## This is for when you have a replicating database setup that you are
145     ## going to test against.  You'll need to define the correct $ENV and have
146     ## two slave databases to test against, as well as a replication system
147     ## that will replicate in less than 1 second.
148     ## --------------------------------------------------------------------- ##
149         
150     package DBIx::Class::DBI::Replicated::TestReplication::Custom; 
151     use base 'DBIx::Class::DBI::Replicated::TestReplication';
152     
153     ## Return an Array of ArrayRefs where each ArrayRef is suitable to use for
154     ## $storage->connect_info to be used for connecting replicants.
155     
156     sub generate_replicant_connect_info { 
157         return (
158             [$ENV{"DBICTEST_SLAVE0_DSN"}, $ENV{"DBICTEST_SLAVE0_DBUSER"}, $ENV{"DBICTEST_SLAVE0_DBPASS"}, {AutoCommit => 1}],
159             [$ENV{"DBICTEST_SLAVE1_DSN"}, $ENV{"DBICTEST_SLAVE1_DBUSER"}, $ENV{"DBICTEST_SLAVE1_DBPASS"}, {AutoCommit => 1}],           
160         );
161     }
162     
163     ## pause a bit to let the replication catch up 
164     
165     sub replicate {
166         sleep 1;
167     } 
168 }
169
170 ## ----------------------------------------------------------------------------
171 ## Create an object and run some tests
172 ## ----------------------------------------------------------------------------
173
174 ## Thi first bunch of tests are basic, just make sure all the bits are behaving
175
176 my $replicated_class = DBICTest->has_custom_dsn ?
177     'DBIx::Class::DBI::Replicated::TestReplication::Custom' :
178     'DBIx::Class::DBI::Replicated::TestReplication::SQLite';
179
180 ok my $replicated = $replicated_class->new
181     => 'Created a replication object';
182     
183 isa_ok $replicated->schema
184     => 'DBIx::Class::Schema';
185     
186 isa_ok $replicated->schema->storage
187     => 'DBIx::Class::Storage::DBI::Replicated';
188
189 ok $replicated->schema->storage->meta
190     => 'has a meta object';
191     
192 isa_ok $replicated->schema->storage->master
193     => 'DBIx::Class::Storage::DBI';
194     
195 isa_ok $replicated->schema->storage->pool
196     => 'DBIx::Class::Storage::DBI::Replicated::Pool';
197     
198 isa_ok $replicated->schema->storage->balancer
199     => 'DBIx::Class::Storage::DBI::Replicated::Balancer'; 
200
201 ok my @replicant_connects = $replicated->generate_replicant_connect_info
202     => 'got replication connect information';
203
204 ok my @replicated_storages = $replicated->schema->storage->connect_replicants(@replicant_connects)
205     => 'Created some storages suitable for replicants';
206     
207 isa_ok $replicated->schema->storage->balancer->current_replicant
208     => 'DBIx::Class::Storage::DBI';
209     
210 ok $replicated->schema->storage->pool->has_replicants
211     => 'does have replicants';     
212
213 is $replicated->schema->storage->num_replicants => 2
214     => 'has two replicants';
215        
216 does_ok $replicated_storages[0]
217     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
218
219 does_ok $replicated_storages[1]
220     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
221     
222 my @replicant_names = keys %{$replicated->schema->storage->replicants};
223
224 does_ok $replicated->schema->storage->replicants->{$replicant_names[0]}
225     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';
226
227 does_ok $replicated->schema->storage->replicants->{$replicant_names[1]}
228     => 'DBIx::Class::Storage::DBI::Replicated::Replicant';  
229
230 ## Add some info to the database
231
232 $replicated
233     ->schema
234     ->populate('Artist', [
235         [ qw/artistid name/ ],
236         [ 4, "Ozric Tentacles"],
237     ]);
238                 
239 ## Make sure all the slaves have the table definitions
240
241 $replicated->replicate;
242
243 ## Make sure we can read the data.
244
245 ok my $artist1 = $replicated->schema->resultset('Artist')->find(4)
246     => 'Created Result';
247
248 isa_ok $artist1
249     => 'DBICTest::Artist';
250     
251 is $artist1->name, 'Ozric Tentacles'
252     => 'Found expected name for first result';
253
254 ## Add some new rows that only the master will have  This is because
255 ## we overload any type of write operation so that is must hit the master
256 ## database.
257
258 $replicated
259     ->schema
260     ->populate('Artist', [
261         [ qw/artistid name/ ],
262         [ 5, "Doom's Children"],
263         [ 6, "Dead On Arrival"],
264         [ 7, "Watergate"],
265     ]);
266
267 SKIP: {
268     ## We can't do this test if we have a custom replicants, since we assume
269     ## if there are custom one that you are trying to test a real replicating
270     ## system.  See docs above for more.
271     
272     skip 'Cannot test inconsistent replication since you have a real replication system', 1
273      if DBICTest->has_custom_dsn && $ENV{"DBICTEST_SLAVE0_DSN"};
274     
275         ## Alright, the database 'cluster' is not in a consistent state.  When we do
276         ## a read now we expect bad news    
277     is $replicated->schema->resultset('Artist')->find(5), undef
278     => 'read after disconnect fails because it uses a replicant which we have neglected to "replicate" yet'; 
279 }
280
281 ## Make sure all the slaves have the table definitions
282 $replicated->replicate;
283
284 ## Should find some data now
285
286 ok my $artist2 = $replicated->schema->resultset('Artist')->find(5)
287     => 'Sync succeed';
288     
289 isa_ok $artist2
290     => 'DBICTest::Artist';
291     
292 is $artist2->name, "Doom's Children"
293     => 'Found expected name for first result';
294
295 ## What happens when we disconnect all the replicants?
296
297 is $replicated->schema->storage->pool->connected_replicants => 2
298     => "both replicants are connected";
299     
300 $replicated->schema->storage->replicants->{$replicant_names[0]}->disconnect;
301 $replicated->schema->storage->replicants->{$replicant_names[1]}->disconnect;
302
303 is $replicated->schema->storage->pool->connected_replicants => 0
304     => "both replicants are now disconnected";
305
306 ## All these should pass, since the database should automatically reconnect
307
308 ok my $artist3 = $replicated->schema->resultset('Artist')->find(6)
309     => 'Still finding stuff.';
310     
311 isa_ok $artist3
312     => 'DBICTest::Artist';
313     
314 is $artist3->name, "Dead On Arrival"
315     => 'Found expected name for first result';
316
317 is $replicated->schema->storage->pool->connected_replicants => 1
318     => "One replicant reconnected to handle the job";
319     
320 ## What happens when we try to select something that doesn't exist?
321
322 ok ! $replicated->schema->resultset('Artist')->find(666)
323     => 'Correctly failed to find something.';
324     
325 ## test the reliable option
326
327 TESTRELIABLE: {
328         
329         $replicated->schema->storage->set_reliable_storage;
330         
331         ok $replicated->schema->resultset('Artist')->find(2)
332             => 'Read from master 1';
333         
334         ok $replicated->schema->resultset('Artist')->find(5)
335             => 'Read from master 2';
336             
337     $replicated->schema->storage->set_balanced_storage;     
338             
339         ok $replicated->schema->resultset('Artist')->find(3)
340         => 'Read from replicant';
341 }
342
343 ## Make sure when reliable goes out of scope, we are using replicants again
344
345 ok $replicated->schema->resultset('Artist')->find(1)
346     => 'back to replicant 1.';
347     
348 ok $replicated->schema->resultset('Artist')->find(2)
349     => 'back to replicant 2.';
350
351 ## set all the replicants to inactive, and make sure the balancer falls back to
352 ## the master.
353
354 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(0);
355 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(0);
356     
357 ok $replicated->schema->resultset('Artist')->find(2)
358     => 'Fallback to master';
359
360 $replicated->schema->storage->replicants->{$replicant_names[0]}->active(1);
361 $replicated->schema->storage->replicants->{$replicant_names[1]}->active(1);
362
363 ok $replicated->schema->resultset('Artist')->find(2)
364     => 'Returned to replicates';
365     
366 ## Getting slave status tests
367
368 SKIP: {
369     ## We skip this tests unless you have a custom replicants, since the default
370     ## sqlite based replication tests don't support these functions.
371     
372     skip 'Cannot Test Replicant Status on Non Replicating Database', 3
373      unless DBICTest->has_custom_dsn && $ENV{"DBICTEST_SLAVE0_DSN"};
374
375     $replicated->replicate; ## Give the slaves a chance to catchup.
376
377         ok $replicated->schema->storage->replicants->{$replicant_names[0]}->is_replicating
378             => 'Replicants are replicating';
379             
380         is $replicated->schema->storage->replicants->{$replicant_names[0]}->lag_behind_master, 0
381             => 'Replicant is zero seconds behind master';
382             
383         ## Test the validate replicants
384         
385         $replicated->schema->storage->pool->validate_replicants;
386         
387         is $replicated->schema->storage->pool->active_replicants, 2
388             => 'Still have 2 replicants after validation';
389             
390         ## Force the replicants to fail the validate test by required their lag to
391         ## be negative (ie ahead of the master!)
392         
393     $replicated->schema->storage->pool->maximum_lag(-10);
394     $replicated->schema->storage->pool->validate_replicants;
395     
396     is $replicated->schema->storage->pool->active_replicants, 0
397         => 'No way a replicant be be ahead of the master';
398         
399     ## Let's be fair to the replicants again.  Let them lag up to 5
400         
401     $replicated->schema->storage->pool->maximum_lag(5);
402     $replicated->schema->storage->pool->validate_replicants;
403     
404     is $replicated->schema->storage->pool->active_replicants, 2
405         => 'Both replicants in good standing again';    
406 }
407
408 ## Delete the old database files
409 $replicated->cleanup;
410
411
412
413
414
415