Port ::Replicated from Moose to Moo+Type::Tiny
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBI / Replicated / Balancer.pm
index 293fe09..8226ff7 100644 (file)
@@ -1,17 +1,22 @@
 package DBIx::Class::Storage::DBI::Replicated::Balancer;
 
-use Moose;
-use List::Util qw(shuffle);
+use Moo::Role;
+requires 'next_storage';
+use Types::Standard qw/Int/;
+use Type::Utils qw/class_type/;
+use Scalar::Util qw/blessed/;
+use DBIx::Class::Storage::DBI::Replicated::Pool;
+use DBIx::Class::Storage::DBI::Replicated::Types qw/DBICStorageDBI/;
+use namespace::clean;
 
 =head1 NAME
 
-DBIx::Class::Storage::DBI::Replicated::Balancer; A Software Load Balancer 
+DBIx::Class::Storage::DBI::Replicated::Balancer - A Software Load Balancer
 
 =head1 SYNOPSIS
 
-This class is used internally by L<DBIx::Class::Storage::DBI::Replicated>.  You
-shouldn't need to create instances of this class.
-    
+This role is used internally by L<DBIx::Class::Storage::DBI::Replicated>.
+
 =head1 DESCRIPTION
 
 Given a pool (L<DBIx::Class::Storage::DBI::Replicated::Pool>) of replicated
@@ -22,35 +27,238 @@ method by which query load can be spread out across each replicant in the pool.
 
 This class defines the following attributes.
 
+=head2 auto_validate_every ($seconds)
+
+If auto_validate has some sort of value, run
+L<DBIx::Class::Storage::DBI::Replicated::Pool/validate_replicants>
+every $seconds.  Be careful with this, because if you set it to 0 you
+will end up validating every query.
+
+=cut
+
+has 'auto_validate_every' => (
+  is=>'rw',
+  isa=>Int,
+  predicate=>'has_auto_validate_every',
+);
+
+=head2 master
+
+The L<DBIx::Class::Storage::DBI> object that is the master database all the
+replicants are trying to follow.  The balancer needs to know it since it's the
+ultimate fallback.
+
+=cut
+
+has 'master' => (
+  is=>'ro',
+  isa=>DBICStorageDBI,
+  required=>1,
+);
+
+=head2 pool
+
+The L<DBIx::Class::Storage::DBI::Replicated::Pool> object that we are trying to
+balance.
+
+=cut
+
+has 'pool' => (
+  is=>'ro',
+  isa=>class_type('DBIx::Class::Storage::DBI::Replicated::Pool'),
+  required=>1,
+);
+
+=head2 current_replicant
+
+Replicant storages (slaves) handle all read only traffic.  The assumption is
+that your database will become readbound well before it becomes write bound
+and that being able to spread your read only traffic around to multiple
+databases is going to help you to scale traffic.
+
+This attribute returns the next slave to handle a read request.  Your L</pool>
+attribute has methods to help you shuffle through all the available replicants
+via its balancer object.
+
+=cut
+
+has 'current_replicant' => (
+  is=> 'rw',
+  isa=>DBICStorageDBI,
+  lazy=>1,
+  builder=>1,
+  predicate=>1,
+  clearer=>1,
+  handles=>[qw/
+    select
+    select_single
+    columns_info_for
+  /],
+);
+
 =head1 METHODS
 
 This class defines the following methods.
 
-=head2 next_storage ($pool)
+=head2 _build_current_replicant
+
+Lazy builder for the L</current_replicant> attribute.
+
+=cut
+
+sub _build_current_replicant {
+  my $self = shift @_;
+  $self->next_storage;
+}
+
+=head2 next_storage
+
+This method should be defined in the class which consumes this role.
 
 Given a pool object, return the next replicant that will serve queries.  The
-default behavior is to randomize but you can write your own subclasses of
-L<DBIx::Class::Storage::DBI::Replicated::Balancer> to support other balance
-systems.
+default behavior is to grab the first replicant it finds but you can write
+your own subclasses of L<DBIx::Class::Storage::DBI::Replicated::Balancer> to
+support other balance systems.
+
+This returns from the pool of active replicants.  If there are no active
+replicants, then you should have it return the master as an ultimate fallback.
+
+=head2 around: next_storage
+
+Advice on next storage to add the autovalidation.  We have this broken out so
+that it's easier to break out the auto validation into a role.
+
+This also returns the master in the case that none of the replicants are active
+or just forgot to create them :)
+
+=cut
+
+my $on_master;
+
+around 'next_storage' => sub {
+  my ($next_storage, $self, @args) = @_;
+  my $now = time;
+
+  ## Do we need to validate the replicants?
+  if(
+     $self->has_auto_validate_every &&
+     ($self->auto_validate_every + $self->pool->last_validated) <= $now
+  ) {
+      $self->pool->validate_replicants;
+  }
+
+  ## Get a replicant, or the master if none
+  if(my $next = $self->$next_storage(@args)) {
+    $self->master->debugobj->print("Moved back to slave\n") if $on_master;
+    $on_master = 0;
+    return $next;
+  } else {
+    $self->master->debugobj->print("No Replicants validate, falling back to master reads.\n")
+       unless $on_master++;
+
+    return $self->master;
+  }
+};
+
+=head2 increment_storage
+
+Rolls the Storage to whatever is next in the queue, as defined by the Balancer.
 
 =cut
 
-sub next_storage {
-       my $self = shift @_;
-       my $pool = shift @_;
-       
-       return (shuffle($pool->all_replicants))[0];
+sub increment_storage {
+  my $self = shift @_;
+  my $next_replicant = $self->next_storage;
+  $self->current_replicant($next_replicant);
 }
 
+=head2 around: select
+
+Advice on the select attribute.  Each time we use a replicant
+we need to change it via the storage pool algorithm.  That way we are spreading
+the load evenly (hopefully) across existing capacity.
+
+=cut
+
+around 'select' => sub {
+  my ($select, $self, @args) = @_;
+
+  if (my $forced_pool = $args[-1]->{force_pool}) {
+    delete $args[-1]->{force_pool};
+    return $self->_get_forced_pool($forced_pool)->select(@args);
+  } elsif($self->master->{transaction_depth}) {
+    return $self->master->select(@args);
+  } else {
+    $self->increment_storage;
+    return $self->$select(@args);
+  }
+};
+
+=head2 around: select_single
+
+Advice on the select_single attribute.  Each time we use a replicant
+we need to change it via the storage pool algorithm.  That way we are spreading
+the load evenly (hopefully) across existing capacity.
+
+=cut
+
+around 'select_single' => sub {
+  my ($select_single, $self, @args) = @_;
+
+  if (my $forced_pool = $args[-1]->{force_pool}) {
+    delete $args[-1]->{force_pool};
+    return $self->_get_forced_pool($forced_pool)->select_single(@args);
+  } elsif($self->master->{transaction_depth}) {
+    return $self->master->select_single(@args);
+  } else {
+    $self->increment_storage;
+    return $self->$select_single(@args);
+  }
+};
+
+=head2 before: columns_info_for
+
+Advice on the current_replicant_storage attribute.  Each time we use a replicant
+we need to change it via the storage pool algorithm.  That way we are spreading
+the load evenly (hopefully) across existing capacity.
+
+=cut
+
+before 'columns_info_for' => sub {
+  my $self = shift @_;
+  $self->increment_storage;
+};
+
+=head2 _get_forced_pool ($name)
+
+Given an identifier, find the most correct storage object to handle the query.
+
+=cut
+
+sub _get_forced_pool {
+  my ($self, $forced_pool) = @_;
+  if(blessed $forced_pool) {
+    return $forced_pool;
+  } elsif($forced_pool eq 'master') {
+    return $self->master;
+  } elsif(my $replicant = $self->pool->replicants->{$forced_pool}) {
+    return $replicant;
+  } else {
+    $self->master->throw_exception("'$forced_pool' is not a named replicant.");
+  }
+}
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-John Napiorkowski <john.napiorkowski@takkle.com>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
-1;
\ No newline at end of file
+1;