use Moose;
use MooseX::AttributeHelpers;
use DBIx::Class::Storage::DBI::Replicated::Replicant;
-use List::Util qw(sum);
+use List::Util 'sum';
+use Scalar::Util 'reftype';
+use Carp::Clan qw/^DBIx::Class/;
+use MooseX::Types::Moose qw/Num Int ClassName HashRef/;
+
+use namespace::clean -except => 'meta';
=head1 NAME
-DBIx::Class::Storage::DBI::Replicated::Pool; Manage a pool of replicants
+DBIx::Class::Storage::DBI::Replicated::Pool - Manage a pool of replicants
=head1 SYNOPSIS
has 'maximum_lag' => (
is=>'rw',
- isa=>'Num',
+ isa=>Num,
required=>1,
lazy=>1,
default=>0,
has 'last_validated' => (
is=>'rw',
- isa=>'Int',
+ isa=>Int,
reader=>'last_validated',
writer=>'_last_validated',
lazy=>1,
has 'replicant_type' => (
is=>'ro',
- isa=>'ClassName',
+ isa=>ClassName,
required=>1,
default=>'DBIx::Class::Storage::DBI',
handles=>{
has 'replicants' => (
is=>'rw',
metaclass => 'Collection::Hash',
- isa=>'HashRef[DBIx::Class::Storage::DBI]',
+ isa=>HashRef['Object'],
default=>sub {{}},
provides => {
'set' => 'set_replicant',
'empty' => 'has_replicants',
'count' => 'num_replicants',
'delete' => 'delete_replicant',
+ 'values' => 'all_replicant_storages',
},
);
=head2 connect_replicants ($schema, Array[$connect_info])
-Given an array of $dsn suitable for connected to a database, create an
-L<DBIx::Class::Storage::DBI::Replicated::Replicant> object and store it in the
-L</replicants> attribute.
+Given an array of $dsn or connect_info structures suitable for connected to a
+database, create an L<DBIx::Class::Storage::DBI::Replicated::Replicant> object
+and store it in the L</replicants> attribute.
=cut
my @newly_created = ();
foreach my $connect_info (@_) {
- my $replicant = $self->create_replicant($schema);
- $replicant->connect_info($connect_info);
- $replicant->ensure_connected;
- DBIx::Class::Storage::DBI::Replicated::Replicant->meta->apply($replicant);
- my ($key) = ($connect_info->[0]=~m/^dbi\:.+\:(.+)$/);
+ $connect_info = [ $connect_info ]
+ if reftype $connect_info ne 'ARRAY';
+
+ croak "coderef replicant connect_info not supported"
+ if ref $connect_info->[0] && reftype $connect_info->[0] eq 'CODE';
+
+ my $replicant = $self->connect_replicant($schema, $connect_info);
+
+ my $key = $connect_info->[0];
+ $key = $key->{dsn} if ref $key && reftype $key eq 'HASH';
+ ($key) = ($key =~ m/^dbi\:.+\:(.+)$/);
+
$self->set_replicant( $key => $replicant);
push @newly_created, $replicant;
}
return @newly_created;
}
+=head2 connect_replicant ($schema, $connect_info)
+
+Given a schema object and a hashref of $connect_info, connect the replicant
+and return it.
+
+=cut
+
+sub connect_replicant {
+ my ($self, $schema, $connect_info) = @_;
+ my $replicant = $self->create_replicant($schema);
+ $replicant->connect_info($connect_info);
+
+## It is undesirable for catalyst to connect at ->conect_replicants time, as
+## connections should only happen on the first request that uses the database.
+## So we try to set the driver without connecting, however this doesn't always
+## work, as a driver may need to connect to determine the DB version, and this
+## may fail.
+##
+## Why this is necessary at all, is that we need to have the final storage
+## class to apply the Replicant role.
+
+ $self->_safely($replicant, '->_determine_driver', sub {
+ $replicant->_determine_driver
+ });
+
+ DBIx::Class::Storage::DBI::Replicated::Replicant->meta->apply($replicant);
+ return $replicant;
+}
+
+=head2 _safely_ensure_connected ($replicant)
+
+The standard ensure_connected method with throw an exception should it fail to
+connect. For the master database this is desirable, but since replicants are
+allowed to fail, this behavior is not desirable. This method wraps the call
+to ensure_connected in an eval in order to catch any generated errors. That
+way a slave can go completely offline (ie, the box itself can die) without
+bringing down your entire pool of databases.
+
+=cut
+
+sub _safely_ensure_connected {
+ my ($self, $replicant, @args) = @_;
+
+ return $self->_safely($replicant, '->ensure_connected', sub {
+ $replicant->ensure_connected(@args)
+ });
+}
+
+=head2 _safely ($replicant, $name, $code)
+
+Execute C<$code> for operation C<$name> catching any exceptions and printing an
+error message to the C<<$replicant->debugobj>>.
+
+Returns 1 on success and undef on failure.
+
+=cut
+
+sub _safely {
+ my ($self, $replicant, $name, $code) = @_;
+
+ eval {
+ $code->()
+ };
+ if ($@) {
+ $replicant
+ ->debugobj
+ ->print(
+ sprintf( "Exception trying to $name for replicant %s, error is %s",
+ $replicant->_dbi_connect_info->[0], $@)
+ );
+ return;
+ }
+ return 1;
+}
+
=head2 connected_replicants
Returns true if there are connected replicants. Actually is overloaded to
Calling this method will generate queries on the replicant databases so it is
not recommended that you run them very often.
+This method requires that your underlying storage engine supports some sort of
+native replication mechanism. Currently only MySQL native replication is
+supported. Your patches to make other replication types work are welcomed.
+
=cut
sub validate_replicants {
my $self = shift @_;
foreach my $replicant($self->all_replicants) {
- if(
- $replicant->is_replicating &&
- $replicant->lag_behind_master <= $self->maximum_lag &&
- $replicant->ensure_connected
- ) {
- ## TODO:: Hook debug for this
- $replicant->active(1)
+ if($self->_safely_ensure_connected($replicant)) {
+ my $is_replicating = $replicant->is_replicating;
+ unless(defined $is_replicating) {
+ $replicant->debugobj->print("Storage Driver ".ref($self)." Does not support the 'is_replicating' method. Assuming you are manually managing.\n");
+ next;
+ } else {
+ if($is_replicating) {
+ my $lag_behind_master = $replicant->lag_behind_master;
+ unless(defined $lag_behind_master) {
+ $replicant->debugobj->print("Storage Driver ".ref($self)." Does not support the 'lag_behind_master' method. Assuming you are manually managing.\n");
+ next;
+ } else {
+ if($lag_behind_master <= $self->maximum_lag) {
+ $replicant->active(1);
+ } else {
+ $replicant->active(0);
+ }
+ }
+ } else {
+ $replicant->active(0);
+ }
+ }
} else {
- ## TODO:: Hook debug for this
$replicant->active(0);
}
}
## Mark that we completed this validation.
- $self->_last_validated(time);
+ $self->_last_validated(time);
}
=head1 AUTHOR
=cut
+__PACKAGE__->meta->make_immutable;
+
1;