package DBIx::Class::Storage::DBI::Replicated;
+BEGIN {
+ use Carp::Clan qw/^DBIx::Class/;
+
+ ## Modules required for Replication support not required for general DBIC
+ ## use, so we explicitly test for these.
+
+ my %replication_required = (
+ 'Moose' => '0.77',
+ 'MooseX::AttributeHelpers' => '0.12',
+ 'MooseX::Types' => '0.10',
+ 'namespace::clean' => '0.11',
+ 'Hash::Merge' => '0.11'
+ );
+
+ my @didnt_load;
+
+ for my $module (keys %replication_required) {
+ eval "use $module $replication_required{$module}";
+ push @didnt_load, "$module $replication_required{$module}"
+ if $@;
+ }
+
+ croak("@{[ join ', ', @didnt_load ]} are missing and are required for Replication")
+ if @didnt_load;
+}
+
use Moose;
-use Class::MOP;
-use Moose::Util::TypeConstraints;
use DBIx::Class::Storage::DBI;
use DBIx::Class::Storage::DBI::Replicated::Pool;
use DBIx::Class::Storage::DBI::Replicated::Balancer;
+use DBIx::Class::Storage::DBI::Replicated::Types qw/BalancerClassNamePart DBICSchema DBICStorageDBI/;
+use MooseX::Types::Moose qw/ClassName HashRef Object/;
+use Scalar::Util 'reftype';
+use Carp::Clan qw/^DBIx::Class/;
+use Hash::Merge 'merge';
+
+use namespace::clean -except => 'meta';
=head1 NAME
-DBIx::Class::Storage::DBI::Replicated - ALPHA Replicated database support
+DBIx::Class::Storage::DBI::Replicated - BETA Replicated database support
=head1 SYNOPSIS
storage type, add some replicated (readonly) databases, and perform reporting
tasks.
- ## Change storage_type in your schema class
+You should set the 'storage_type attribute to a replicated type. You should
+also defined you arguments, such as which balancer you want and any arguments
+that the Pool object should get.
+
$schema->storage_type( ['::DBI::Replicated', {balancer=>'::Random'}] );
- ## Add some slaves. Basically this is an array of arrayrefs, where each
- ## arrayref is database connect information
+Next, you need to add in the Replicants. Basically this is an array of
+arrayrefs, where each arrayref is database connect information. Think of these
+arguments as what you'd pass to the 'normal' $schema->connect method.
$schema->storage->connect_replicants(
[$dsn1, $user, $pass, \%opts],
[$dsn3, $user, $pass, \%opts],
);
- ## Now, just use the $schema as normal
+Now, just use the $schema as you normally would. Automatically all reads will
+be delegated to the replicants, while writes to the master.
+
$schema->resultset('Source')->search({name=>'etc'});
- ## You can force a given query to use a particular storage using the search
- ### attribute 'force_pool'. For example:
+You can force a given query to use a particular storage using the search
+attribute 'force_pool'. For example:
my $RS = $schema->resultset('Source')->search(undef, {force_pool=>'master'});
-
- ## Now $RS will force everything (both reads and writes) to use whatever was
- ## setup as the master storage. 'master' is hardcoded to always point to the
- ## Master, but you can also use any Replicant name. Please see:
- ## L<DBIx::Class::Storage::Replicated::Pool> and the replicants attribute for
- ## More. Also see transactions and L</execute_reliably> for alternative ways
- ## to force read traffic to the master.
+
+Now $RS will force everything (both reads and writes) to use whatever was setup
+as the master storage. 'master' is hardcoded to always point to the Master,
+but you can also use any Replicant name. Please see:
+L<DBIx::Class::Storage::DBI::Replicated::Pool> and the replicants attribute for more.
+
+Also see transactions and L</execute_reliably> for alternative ways to
+force read traffic to the master. In general, you should wrap your statements
+in a transaction when you are reading and writing to the same tables at the
+same time, since your replicants will often lag a bit behind the master.
+
+See L<DBIx::Class::Storage::DBI::Replicated::Instructions> for more help and
+walkthroughs.
=head1 DESCRIPTION
Warning: This class is marked BETA. This has been running a production
-website using MySQL native replication as it's backend and we have some decent
+website using MySQL native replication as its backend and we have some decent
test coverage but the code hasn't yet been stressed by a variety of databases.
Individual DB's may have quirks we are not aware of. Please use this in first
development and pass along your experiences/bug fixes.
The consistancy betweeen master and replicants is database specific. The Pool
gives you a method to validate it's replicants, removing and replacing them
when they fail/pass predefined criteria. Please make careful use of the ways
-to force a query to run against Master when needed.
+to force a query to run against Master when needed.
+
+=head1 REQUIREMENTS
+
+Replicated Storage has additional requirements not currently part of L<DBIx::Class>
+
+ Moose => 0.77
+ MooseX::AttributeHelpers => 0.12
+ MooseX::Types => 0.10
+ namespace::clean => 0.11
+ Hash::Merge => 0.11
+
+You will need to install these modules manually via CPAN or make them part of the
+Makefile for your distribution.
=head1 ATTRIBUTES
has 'schema' => (
is=>'rw',
- isa=>'DBIx::Class::Schema',
+ isa=>DBICSchema,
weak_ref=>1,
required=>1,
);
=cut
has 'pool_type' => (
- is=>'ro',
- isa=>'ClassName',
- required=>1,
+ is=>'rw',
+ isa=>ClassName,
default=>'DBIx::Class::Storage::DBI::Replicated::Pool',
handles=>{
'create_pool' => 'new',
=head2 pool_args
Contains a hashref of initialized information to pass to the Balancer object.
-See L<DBIx::Class::Storage::Replicated::Pool> for available arguments.
+See L<DBIx::Class::Storage::DBI::Replicated::Pool> for available arguments.
=cut
has 'pool_args' => (
- is=>'ro',
- isa=>'HashRef',
+ is=>'rw',
+ isa=>HashRef,
lazy=>1,
- required=>1,
default=>sub { {} },
);
=cut
-subtype 'DBIx::Class::Storage::DBI::Replicated::BalancerClassNamePart',
- as 'ClassName';
-
-coerce 'DBIx::Class::Storage::DBI::Replicated::BalancerClassNamePart',
- from 'Str',
- via {
- my $type = $_;
- if($type=~m/^::/) {
- $type = 'DBIx::Class::Storage::DBI::Replicated::Balancer'.$type;
- }
- Class::MOP::load_class($type);
- $type;
- };
-
has 'balancer_type' => (
- is=>'ro',
- isa=>'DBIx::Class::Storage::DBI::Replicated::BalancerClassNamePart',
+ is=>'rw',
+ isa=>BalancerClassNamePart,
coerce=>1,
required=>1,
default=> 'DBIx::Class::Storage::DBI::Replicated::Balancer::First',
=head2 balancer_args
Contains a hashref of initialized information to pass to the Balancer object.
-See L<DBIx::Class::Storage::Replicated::Balancer> for available arguments.
+See L<DBIx::Class::Storage::DBI::Replicated::Balancer> for available arguments.
=cut
has 'balancer_args' => (
- is=>'ro',
- isa=>'HashRef',
+ is=>'rw',
+ isa=>HashRef,
lazy=>1,
required=>1,
default=>sub { {} },
=cut
has 'balancer' => (
- is=>'ro',
+ is=>'rw',
isa=>'DBIx::Class::Storage::DBI::Replicated::Balancer',
lazy_build=>1,
handles=>[qw/auto_validate_every/],
has 'master' => (
is=> 'ro',
- isa=>'DBIx::Class::Storage::DBI',
+ isa=>DBICStorageDBI,
lazy_build=>1,
);
has 'read_handler' => (
is=>'rw',
- isa=>'Object',
+ isa=>Object,
lazy_build=>1,
handles=>[qw/
select
has 'write_handler' => (
is=>'ro',
- isa=>'Object',
- lazy_build=>1,
+ isa=>Object,
lazy_build=>1,
handles=>[qw/
on_connect_do
create_ddl_dir
deployment_statements
datetime_parser
- datetime_parser_type
+ datetime_parser_type
+ build_datetime_parser
last_insert_id
insert
insert_bulk
txn_scope_guard
sth
deploy
-
+ with_deferred_fk_checks
+ dbh_do
reload_row
+ with_deferred_fk_checks
_prep_for_execute
- configure_sqlt
-
+
+ backup
+ is_datatype_numeric
+ _count_select
+ _subq_count_select
+ _subq_update_delete
+ svp_rollback
+ svp_begin
+ svp_release
/],
);
+has _master_connect_info_opts =>
+ (is => 'rw', isa => HashRef, default => sub { {} });
+
+=head2 around: connect_info
+
+Preserve master's C<connect_info> options (for merging with replicants.)
+Also set any Replicated related options from connect_info, such as
+C<pool_type>, C<pool_args>, C<balancer_type> and C<balancer_args>.
+
+=cut
+
+around connect_info => sub {
+ my ($next, $self, $info, @extra) = @_;
+
+ my $wantarray = wantarray;
+
+ my %opts;
+ for my $arg (@$info) {
+ next unless (reftype($arg)||'') eq 'HASH';
+ %opts = %{ merge($arg, \%opts) };
+ }
+ delete $opts{dsn};
+
+ if (@opts{qw/pool_type pool_args/}) {
+ $self->pool_type(delete $opts{pool_type})
+ if $opts{pool_type};
+
+ $self->pool_args(
+ merge((delete $opts{pool_args} || {}), $self->pool_args)
+ );
+
+ $self->pool($self->_build_pool)
+ if $self->pool;
+ }
+
+ if (@opts{qw/balancer_type balancer_args/}) {
+ $self->balancer_type(delete $opts{balancer_type})
+ if $opts{balancer_type};
+
+ $self->balancer_args(
+ merge((delete $opts{balancer_args} || {}), $self->balancer_args)
+ );
+
+ $self->balancer($self->_build_balancer)
+ if $self->balancer;
+ }
+
+ $self->_master_connect_info_opts(\%opts);
+
+ my (@res, $res);
+ if ($wantarray) {
+ @res = $self->$next($info, @extra);
+ } else {
+ $res = $self->$next($info, @extra);
+ }
+
+ # Make sure master is blessed into the correct class and apply role to it.
+ my $master = $self->master;
+ $master->_determine_driver;
+ Moose::Meta::Class->initialize(ref $master);
+ DBIx::Class::Storage::DBI::Replicated::WithDSN->meta->apply($master);
+
+ $wantarray ? @res : $res;
+};
+
=head1 METHODS
This class defines the following methods.
sub _build_master {
my $self = shift @_;
- DBIx::Class::Storage::DBI->new($self->schema);
+ my $master = DBIx::Class::Storage::DBI->new($self->schema);
+ $master
}
=head2 _build_pool
=head2 around: connect_replicants
All calls to connect_replicants needs to have an existing $schema tacked onto
-top of the args, since L<DBIx::Storage::DBI> needs it.
+top of the args, since L<DBIx::Storage::DBI> needs it, and any C<connect_info>
+options merged with the master, with replicant opts having higher priority.
=cut
-around 'connect_replicants' => sub {
- my ($method, $self, @args) = @_;
- $self->$method($self->schema, @args);
+around connect_replicants => sub {
+ my ($next, $self, @args) = @_;
+
+ for my $r (@args) {
+ $r = [ $r ] unless reftype $r eq 'ARRAY';
+
+ croak "coderef replicant connect_info not supported"
+ if ref $r->[0] && reftype $r->[0] eq 'CODE';
+
+# any connect_info options?
+ my $i = 0;
+ $i++ while $i < @$r && (reftype($r->[$i])||'') ne 'HASH';
+
+# make one if none
+ $r->[$i] = {} unless $r->[$i];
+
+# merge if two hashes
+ my @hashes = @$r[$i .. $#{$r}];
+
+ croak "invalid connect_info options"
+ if (grep { reftype($_) eq 'HASH' } @hashes) != @hashes;
+
+ croak "too many hashrefs in connect_info"
+ if @hashes > 2;
+
+ my %opts = %{ merge(reverse @hashes) };
+
+# delete them
+ splice @$r, $i+1, ($#{$r} - $i), ();
+
+# merge with master
+ %opts = %{ merge(\%opts, $self->_master_connect_info_opts) };
+
+# update
+ $r->[$i] = \%opts;
+ }
+
+ $self->$next($self->schema, @args);
};
=head2 all_storages
my $self = shift @_;
return grep {defined $_ && blessed $_} (
$self->master,
- $self->replicants,
+ values %{ $self->replicants },
);
}
sub set_balanced_storage {
my $self = shift @_;
my $schema = $self->schema;
- my $write_handler = $self->schema->storage->balancer;
+ my $balanced_handler = $self->schema->storage->balancer;
- $schema->storage->read_handler($write_handler);
+ $schema->storage->read_handler($balanced_handler);
}
-=head2 around: txn_do ($coderef)
-
-Overload to the txn_do method, which is delegated to whatever the
-L<write_handler> is set to. We overload this in order to wrap in inside a
-L</execute_reliably> method.
-
-=cut
-
-around 'txn_do' => sub {
- my($txn_do, $self, $coderef, @args) = @_;
- $self->execute_reliably(sub {$self->$txn_do($coderef, @args)});
-};
-
=head2 connected
Check that the master and at least one of the replicants is connected.
foreach my $source ($self->all_storages) {
$source->limit_dialect(@_);
}
+ return $self->master->quote_char;
}
=head2 quote_char
foreach my $source ($self->all_storages) {
$source->quote_char(@_);
}
+ return $self->master->quote_char;
}
=head2 name_sep
foreach my $source ($self->all_storages) {
$source->name_sep(@_);
}
+ return $self->master->name_sep;
}
=head2 set_schema
sub debug {
my $self = shift @_;
- foreach my $source ($self->all_storages) {
- $source->debug(@_);
+ if(@_) {
+ foreach my $source ($self->all_storages) {
+ $source->debug(@_);
+ }
}
+ return $self->master->debug;
}
=head2 debugobj
sub debugobj {
my $self = shift @_;
- foreach my $source ($self->all_storages) {
- $source->debugobj(@_);
+ if(@_) {
+ foreach my $source ($self->all_storages) {
+ $source->debugobj(@_);
+ }
}
+ return $self->master->debugobj;
}
=head2 debugfh
sub debugfh {
my $self = shift @_;
- foreach my $source ($self->all_storages) {
- $source->debugfh(@_);
+ if(@_) {
+ foreach my $source ($self->all_storages) {
+ $source->debugfh(@_);
+ }
}
+ return $self->master->debugfh;
}
=head2 debugcb
sub debugcb {
my $self = shift @_;
- foreach my $source ($self->all_storages) {
- $source->debugcb(@_);
+ if(@_) {
+ foreach my $source ($self->all_storages) {
+ $source->debugcb(@_);
+ }
}
+ return $self->master->debugcb;
}
=head2 disconnect
}
}
+=head2 cursor_class
+
+set cursor class on all storages, or return master's
+
+=cut
+
+sub cursor_class {
+ my ($self, $cursor_class) = @_;
+
+ if ($cursor_class) {
+ $_->cursor_class($cursor_class) for $self->all_storages;
+ }
+ $self->master->cursor_class;
+}
+
=head1 GOTCHAS
Due to the fact that replicants can lag behind a master, you must take care to