1 package DBIx::Class::Storage::DBI::Replicated::Pool;
4 use DBIx::Class::Storage::DBI::Replicated::Replicant;
6 use Scalar::Util 'reftype';
8 use Types::Standard qw/Num Int ClassName HashRef Object/;
9 use DBIx::Class::Storage::DBI::Replicated::Types 'DBICStorageDBI';
12 use namespace::clean -except => 'meta';
16 DBIx::Class::Storage::DBI::Replicated::Pool - Manage a pool of replicants
20 This class is used internally by L<DBIx::Class::Storage::DBI::Replicated>. You
21 shouldn't need to create instances of this class.
25 In a replicated storage type, there is at least one replicant to handle the
26 read-only traffic. The Pool class manages this replicant, or list of
27 replicants, and gives some methods for querying information about their status.
31 This class defines the following attributes.
33 =head2 maximum_lag ($num)
35 This is a number which defines the maximum allowed lag returned by the
36 L<DBIx::Class::Storage::DBI/lag_behind_master> method. The default is 0. In
37 general, this should return a larger number when the replicant is lagging
38 behind its master, however the implementation of this is database specific, so
39 don't count on this number having a fixed meaning. For example, MySQL will
40 return a number of seconds that the replicating database is lagging.
44 has 'maximum_lag' => (
53 This is an integer representing a time since the last time the replicants were
54 validated. It's nothing fancy, just an integer provided via the perl L<time|perlfunc/time>
59 has 'last_validated' => (
62 reader=>'last_validated',
63 writer=>'_last_validated',
68 =head2 replicant_type ($classname)
70 Base class used to instantiate replicants that are in the pool. Unless you
71 need to subclass L<DBIx::Class::Storage::DBI::Replicated::Replicant> you should
72 just leave this alone.
76 has 'replicant_type' => (
80 default=>'DBIx::Class::Storage::DBI',
82 'create_replicant' => 'new',
88 A hashref of replicant, with the key being the dsn and the value returning the
89 actual replicant storage. For example, if the $dsn element is something like:
91 "dbi:SQLite:dbname=dbfile"
93 You could access the specific replicant via:
95 $schema->storage->replicants->{'dbname=dbfile'}
97 This attributes also supports the following helper methods:
101 =item set_replicant($key=>$storage)
103 Pushes a replicant onto the HashRef under $key
105 =item get_replicant($key)
107 Retrieves the named replicant
111 Returns true if the Pool defines replicants.
115 The number of replicants in the pool
117 =item delete_replicant ($key)
119 Removes the replicant under $key from the pool
125 has 'replicants' => (
127 isa=>HashRef[Object],
131 sub set_replicant { $_[0]->replicants->{$_[1]} = $_[2] }
132 sub get_replicant { $_[0]->replicants->{$_[1]} }
133 sub has_replicants { !!keys %{$_[0]->replicants} }
134 sub num_replicants { 0+keys %{$_[0]->replicants} }
135 sub delete_replicant { delete $_[0]->replicants->{$_[1]} }
136 sub all_replicant_storages { values %{$_[0]->replicants} }
138 has next_unknown_replicant_id => (
144 sub inc_unknown_replicant_id {
146 $self->next_unknown_replicant_id($self->next_unknown_replicant_id + 1);
151 Reference to the master Storage.
155 has master => (is => 'rw', isa => DBICStorageDBI, weak_ref => 1);
159 This class defines the following methods.
161 =head2 connect_replicants ($schema, Array[$connect_info])
163 Given an array of $dsn or connect_info structures suitable for connected to a
164 database, create an L<DBIx::Class::Storage::DBI::Replicated::Replicant> object
165 and store it in the L</replicants> attribute.
169 sub connect_replicants {
171 my $schema = shift @_;
173 my @newly_created = ();
174 foreach my $connect_info (@_) {
175 $connect_info = [ $connect_info ]
176 if reftype $connect_info ne 'ARRAY';
178 my $connect_coderef =
179 (reftype($connect_info->[0])||'') eq 'CODE' ? $connect_info->[0]
180 : (reftype($connect_info->[0])||'') eq 'HASH' &&
181 $connect_info->[0]->{dbh_maker};
185 # yes this is evil, but it only usually happens once (for coderefs)
186 # this will fail if the coderef does not actually DBI::connect
187 no warnings 'redefine';
188 my $connect = \&DBI::connect;
189 local *DBI::connect = sub {
193 $self->connect_replicant($schema, $connect_info);
199 if (!$connect_coderef) {
200 $dsn = $connect_info->[0];
201 $dsn = $dsn->{dsn} if (reftype($dsn)||'') eq 'HASH';
204 # all attempts to get the DSN failed
205 $key = "UNKNOWN_" . $self->next_unknown_replicant_id;
206 $self->inc_unknown_replicant_id;
210 $replicant->dsn($dsn);
211 ($key) = ($dsn =~ m/^dbi\:.+\:(.+)$/i);
214 $replicant->id($key);
215 $self->set_replicant($key => $replicant);
217 push @newly_created, $replicant;
220 return @newly_created;
223 =head2 connect_replicant ($schema, $connect_info)
225 Given a schema object and a hashref of $connect_info, connect the replicant
230 sub connect_replicant {
231 my ($self, $schema, $connect_info) = @_;
232 my $replicant = $self->create_replicant($schema);
233 $replicant->connect_info($connect_info);
235 ## It is undesirable for catalyst to connect at ->conect_replicants time, as
236 ## connections should only happen on the first request that uses the database.
237 ## So we try to set the driver without connecting, however this doesn't always
238 ## work, as a driver may need to connect to determine the DB version, and this
241 ## Why this is necessary at all, is that we need to have the final storage
242 ## class to apply the Replicant role.
244 $self->_safely($replicant, '->_determine_driver', sub {
245 $replicant->_determine_driver
248 Moo::Role->apply_roles_to_object(
250 'DBIx::Class::Storage::DBI::Replicated::Replicant',
253 # link back to master
254 $replicant->master($self->master);
259 =head2 _safely_ensure_connected ($replicant)
261 The standard ensure_connected method with throw an exception should it fail to
262 connect. For the master database this is desirable, but since replicants are
263 allowed to fail, this behavior is not desirable. This method wraps the call
264 to ensure_connected in an eval in order to catch any generated errors. That
265 way a slave can go completely offline (e.g. the box itself can die) without
266 bringing down your entire pool of databases.
270 sub _safely_ensure_connected {
271 my ($self, $replicant, @args) = @_;
273 return $self->_safely($replicant, '->ensure_connected', sub {
274 $replicant->ensure_connected(@args)
278 =head2 _safely ($replicant, $name, $code)
280 Execute C<$code> for operation C<$name> catching any exceptions and printing an
281 error message to the C<<$replicant->debugobj>>.
283 Returns 1 on success and undef on failure.
288 my ($self, $replicant, $name, $code) = @_;
294 $replicant->debugobj->print(sprintf(
295 "Exception trying to $name for replicant %s, error is %s",
296 $replicant->_dbi_connect_info->[0], $_)
302 =head2 connected_replicants
304 Returns true if there are connected replicants. Actually is overloaded to
305 return the number of replicants. So you can do stuff like:
307 if( my $num_connected = $storage->has_connected_replicants ) {
308 print "I have $num_connected connected replicants";
310 print "Sorry, no replicants.";
313 This method will actually test that each replicant in the L</replicants> hashref
314 is actually connected, try not to hit this 10 times a second.
318 sub connected_replicants {
322 } $self->all_replicants );
325 =head2 active_replicants
327 This is an array of replicants that are considered to be active in the pool.
328 This does not check to see if they are connected, but if they are not, DBIC
329 should automatically reconnect them for us when we hit them with a query.
333 sub active_replicants {
335 return ( grep {$_} map {
337 } $self->all_replicants );
340 =head2 all_replicants
342 Just a simple array of all the replicant storages. No particular order to the
343 array is given, nor should any meaning be derived.
349 return values %{$self->replicants};
352 =head2 validate_replicants
354 This does a check to see if 1) each replicate is connected (or reconnectable),
355 2) that is ->is_replicating, and 3) that it is not exceeding the lag amount
356 defined by L</maximum_lag>. Replicants that fail any of these tests are set to
357 inactive, and thus removed from the replication pool.
359 This tests L</all_replicants>, since a replicant that has been previous marked
360 as inactive can be reactivated should it start to pass the validation tests again.
362 See L<DBIx::Class::Storage::DBI> for more about checking if a replicating
363 connection is not following a master or is lagging.
365 Calling this method will generate queries on the replicant databases so it is
366 not recommended that you run them very often.
368 This method requires that your underlying storage engine supports some sort of
369 native replication mechanism. Currently only MySQL native replication is
370 supported. Your patches to make other replication types work are welcomed.
374 sub validate_replicants {
376 foreach my $replicant($self->all_replicants) {
377 if($self->_safely_ensure_connected($replicant)) {
378 my $is_replicating = $replicant->is_replicating;
379 unless(defined $is_replicating) {
380 $replicant->debugobj->print("Storage Driver ".ref($self)." Does not support the 'is_replicating' method. Assuming you are manually managing.\n");
383 if($is_replicating) {
384 my $lag_behind_master = $replicant->lag_behind_master;
385 unless(defined $lag_behind_master) {
386 $replicant->debugobj->print("Storage Driver ".ref($self)." Does not support the 'lag_behind_master' method. Assuming you are manually managing.\n");
389 if($lag_behind_master <= $self->maximum_lag) {
390 $replicant->active(1);
392 $replicant->active(0);
396 $replicant->active(0);
400 $replicant->active(0);
403 ## Mark that we completed this validation.
404 $self->_last_validated(time);
407 =head1 FURTHER QUESTIONS?
409 Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
411 =head1 COPYRIGHT AND LICENSE
413 This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
414 by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
415 redistribute it and/or modify it under the same terms as the
416 L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.