15be166385e584c8a390f4e29a3880a5d9b723ae
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBI / Replicated / Pool.pm
1 package DBIx::Class::Storage::DBI::Replicated::Pool;
2
3 use Moose;
4 use MooseX::AttributeHelpers;
5 use DBIx::Class::Storage::DBI::Replicated::Replicant;
6 use List::Util 'sum';
7 use Scalar::Util 'reftype';
8 use Carp::Clan qw/^DBIx::Class/;
9
10 use namespace::clean -except => 'meta';
11
12 =head1 NAME
13
14 DBIx::Class::Storage::DBI::Replicated::Pool - Manage a pool of replicants
15
16 =head1 SYNOPSIS
17
18 This class is used internally by L<DBIx::Class::Storage::DBI::Replicated>.  You
19 shouldn't need to create instances of this class.
20   
21 =head1 DESCRIPTION
22
23 In a replicated storage type, there is at least one replicant to handle the
24 read only traffic.  The Pool class manages this replicant, or list of 
25 replicants, and gives some methods for querying information about their status.
26
27 =head1 ATTRIBUTES
28
29 This class defines the following attributes.
30
31 =head2 maximum_lag ($num)
32
33 This is a number which defines the maximum allowed lag returned by the
34 L<DBIx::Class::Storage::DBI/lag_behind_master> method.  The default is 0.  In
35 general, this should return a larger number when the replicant is lagging
36 behind it's master, however the implementation of this is database specific, so
37 don't count on this number having a fixed meaning.  For example, MySQL will
38 return a number of seconds that the replicating database is lagging.
39
40 =cut
41
42 has 'maximum_lag' => (
43   is=>'rw',
44   isa=>'Num',
45   required=>1,
46   lazy=>1,
47   default=>0,
48 );
49
50 =head2 last_validated
51
52 This is an integer representing a time since the last time the replicants were
53 validated. It's nothing fancy, just an integer provided via the perl time 
54 builtin.
55
56 =cut
57
58 has 'last_validated' => (
59   is=>'rw',
60   isa=>'Int',
61   reader=>'last_validated',
62   writer=>'_last_validated',
63   lazy=>1,
64   default=>0,
65 );
66
67 =head2 replicant_type ($classname)
68
69 Base class used to instantiate replicants that are in the pool.  Unless you
70 need to subclass L<DBIx::Class::Storage::DBI::Replicated::Replicant> you should
71 just leave this alone.
72
73 =cut
74
75 has 'replicant_type' => (
76   is=>'ro',
77   isa=>'ClassName',
78   required=>1,
79   default=>'DBIx::Class::Storage::DBI',
80   handles=>{
81     'create_replicant' => 'new',
82   },  
83 );
84
85 =head2 replicants
86
87 A hashref of replicant, with the key being the dsn and the value returning the
88 actual replicant storage.  For example if the $dsn element is something like:
89
90   "dbi:SQLite:dbname=dbfile"
91   
92 You could access the specific replicant via:
93
94   $schema->storage->replicants->{'dbname=dbfile'}
95   
96 This attributes also supports the following helper methods:
97
98 =over 4
99
100 =item set_replicant($key=>$storage)
101
102 Pushes a replicant onto the HashRef under $key
103
104 =item get_replicant($key)
105
106 Retrieves the named replicant
107
108 =item has_replicants
109
110 Returns true if the Pool defines replicants.
111
112 =item num_replicants
113
114 The number of replicants in the pool
115
116 =item delete_replicant ($key)
117
118 removes the replicant under $key from the pool
119
120 =back
121
122 =cut
123
124 has 'replicants' => (
125   is=>'rw',
126   metaclass => 'Collection::Hash',
127   isa=>'HashRef[DBIx::Class::Storage::DBI]',
128   default=>sub {{}},
129   provides  => {
130     'set' => 'set_replicant',
131     'get' => 'get_replicant',            
132     'empty' => 'has_replicants',
133     'count' => 'num_replicants',
134     'delete' => 'delete_replicant',
135   },
136 );
137
138 =head1 METHODS
139
140 This class defines the following methods.
141
142 =head2 connect_replicants ($schema, Array[$connect_info])
143
144 Given an array of $dsn suitable for connected to a database, create an
145 L<DBIx::Class::Storage::DBI::Replicated::Replicant> object and store it in the
146 L</replicants> attribute.
147
148 =cut
149
150 sub connect_replicants {
151   my $self = shift @_;
152   my $schema = shift @_;
153   
154   my @newly_created = ();
155   foreach my $connect_info (@_) {
156     $connect_info = [ $connect_info ]
157       if reftype $connect_info ne 'ARRAY';
158
159     croak "coderef connect_info not supported"
160       if ref $connect_info->[0] && reftype $connect_info->[0] eq 'CODE';
161
162     my $replicant = $self->connect_replicant($schema, $connect_info);
163
164     my $key = $connect_info->[0];
165     $key = $key->{dsn} if ref $key && reftype $key eq 'HASH';
166     ($key) = ($key =~ m/^dbi\:.+\:(.+)$/);
167
168     $self->set_replicant( $key => $replicant);  
169     push @newly_created, $replicant;
170   }
171   
172   return @newly_created;
173 }
174
175 =head2 connect_replicant ($schema, $connect_info)
176
177 Given a schema object and a hashref of $connect_info, connect the replicant
178 and return it.
179
180 =cut
181
182 sub connect_replicant {
183   my ($self, $schema, $connect_info) = @_;
184   my $replicant = $self->create_replicant($schema);
185   $replicant->connect_info($connect_info);
186   $self->_safely_ensure_connected($replicant);
187   DBIx::Class::Storage::DBI::Replicated::Replicant->meta->apply($replicant);  
188   return $replicant;
189 }
190
191 =head2 _safely_ensure_connected ($replicant)
192
193 The standard ensure_connected method with throw an exception should it fail to
194 connect.  For the master database this is desirable, but since replicants are
195 allowed to fail, this behavior is not desirable.  This method wraps the call
196 to ensure_connected in an eval in order to catch any generated errors.  That
197 way a slave to go completely offline (ie, the box itself can die) without
198 bringing down your entire pool of databases.
199
200 =cut
201
202 sub _safely_ensure_connected {
203   my ($self, $replicant, @args) = @_;
204   eval {
205     $replicant->ensure_connected(@args);
206   }; 
207   if ($@) {
208     $replicant
209       ->debugobj
210       ->print(
211         sprintf( "Exception trying to ->ensure_connected for replicant %s, error is %s",
212           $replicant->_dbi_connect_info->[0], $@)
213         );
214         return;
215   }
216   return 1;
217 }
218
219 =head2 connected_replicants
220
221 Returns true if there are connected replicants.  Actually is overloaded to
222 return the number of replicants.  So you can do stuff like:
223
224   if( my $num_connected = $storage->has_connected_replicants ) {
225     print "I have $num_connected connected replicants";
226   } else {
227     print "Sorry, no replicants.";
228   }
229
230 This method will actually test that each replicant in the L</replicants> hashref
231 is actually connected, try not to hit this 10 times a second.
232
233 =cut
234
235 sub connected_replicants {
236   my $self = shift @_;
237   return sum( map {
238     $_->connected ? 1:0
239   } $self->all_replicants );
240 }
241
242 =head2 active_replicants
243
244 This is an array of replicants that are considered to be active in the pool.
245 This does not check to see if they are connected, but if they are not, DBIC
246 should automatically reconnect them for us when we hit them with a query.
247
248 =cut
249
250 sub active_replicants {
251   my $self = shift @_;
252   return ( grep {$_} map {
253     $_->active ? $_:0
254   } $self->all_replicants );
255 }
256
257 =head2 all_replicants
258
259 Just a simple array of all the replicant storages.  No particular order to the
260 array is given, nor should any meaning be derived.
261
262 =cut
263
264 sub all_replicants {
265   my $self = shift @_;
266   return values %{$self->replicants};
267 }
268
269 =head2 validate_replicants
270
271 This does a check to see if 1) each replicate is connected (or reconnectable),
272 2) that is ->is_replicating, and 3) that it is not exceeding the lag amount
273 defined by L</maximum_lag>.  Replicants that fail any of these tests are set to
274 inactive, and thus removed from the replication pool.
275
276 This tests L<all_replicants>, since a replicant that has been previous marked
277 as inactive can be reactived should it start to pass the validation tests again.
278
279 See L<DBIx::Class::Storage::DBI> for more about checking if a replicating
280 connection is not following a master or is lagging.
281
282 Calling this method will generate queries on the replicant databases so it is
283 not recommended that you run them very often.
284
285 This method requires that your underlying storage engine supports some sort of
286 native replication mechanism.  Currently only MySQL native replication is
287 supported.  Your patches to make other replication types work are welcomed.
288
289 =cut
290
291 sub validate_replicants {
292   my $self = shift @_;
293   foreach my $replicant($self->all_replicants) {
294     if($self->_safely_ensure_connected($replicant)) {
295       my $is_replicating = $replicant->is_replicating;
296       unless(defined $is_replicating) {
297         $replicant->debugobj->print("Storage Driver ".ref($self)." Does not support the 'is_replicating' method.  Assuming you are manually managing.\n");
298         next;
299       } else {
300         if($is_replicating) {
301           my $lag_behind_master = $replicant->lag_behind_master;
302           unless(defined $lag_behind_master) {
303             $replicant->debugobj->print("Storage Driver ".ref($self)." Does not support the 'lag_behind_master' method.  Assuming you are manually managing.\n");
304             next;
305           } else {
306             if($lag_behind_master <= $self->maximum_lag) {
307               $replicant->active(1);
308             } else {
309               $replicant->active(0);  
310             }
311           }    
312         } else {
313           $replicant->active(0);
314         }
315       }
316     } else {
317       $replicant->active(0);
318     }
319   }
320   ## Mark that we completed this validation.  
321   $self->_last_validated(time);  
322 }
323
324 =head1 AUTHOR
325
326 John Napiorkowski <john.napiorkowski@takkle.com>
327
328 =head1 LICENSE
329
330 You may distribute this code under the same terms as Perl itself.
331
332 =cut
333
334 __PACKAGE__->meta->make_immutable;
335
336 1;