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