Improve fallback-to-master/return-to-slave reporting in ::Replicated::Balancer
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBI / Replicated / Balancer.pm
CommitLineData
26ab719a 1package DBIx::Class::Storage::DBI::Replicated::Balancer;
2
17b05c13 3use Moose::Role;
4requires 'next_storage';
41916570 5use MooseX::Types::Moose qw/Int/;
6a151f58 6use DBIx::Class::Storage::DBI::Replicated::Pool;
7use DBIx::Class::Storage::DBI::Replicated::Types qw/DBICStorageDBI/;
41916570 8use namespace::clean -except => 'meta';
26ab719a 9
10=head1 NAME
11
21fc4719 12DBIx::Class::Storage::DBI::Replicated::Balancer - A Software Load Balancer
26ab719a 13
14=head1 SYNOPSIS
15
17b05c13 16This role is used internally by L<DBIx::Class::Storage::DBI::Replicated>.
d4daee7b 17
26ab719a 18=head1 DESCRIPTION
19
20Given a pool (L<DBIx::Class::Storage::DBI::Replicated::Pool>) of replicated
21database's (L<DBIx::Class::Storage::DBI::Replicated::Replicant>), defines a
22method by which query load can be spread out across each replicant in the pool.
23
24=head1 ATTRIBUTES
25
26This class defines the following attributes.
27
7edf5f1c 28=head2 auto_validate_every ($seconds)
29
30If auto_validate has some sort of value, run the L<validate_replicants> every
31$seconds. Be careful with this, because if you set it to 0 you will end up
32validating every query.
33
34=cut
35
36has 'auto_validate_every' => (
64cdad22 37 is=>'rw',
41916570 38 isa=>Int,
64cdad22 39 predicate=>'has_auto_validate_every',
7edf5f1c 40);
41
106d5f3b 42=head2 master
43
44The L<DBIx::Class::Storage::DBI> object that is the master database all the
45replicants are trying to follow. The balancer needs to know it since it's the
46ultimate fallback.
47
48=cut
49
50has 'master' => (
64cdad22 51 is=>'ro',
6a151f58 52 isa=>DBICStorageDBI,
64cdad22 53 required=>1,
106d5f3b 54);
55
cb6ec758 56=head2 pool
57
58The L<DBIx::Class::Storage::DBI::Replicated::Pool> object that we are trying to
59balance.
60
61=cut
62
63has 'pool' => (
64cdad22 64 is=>'ro',
65 isa=>'DBIx::Class::Storage::DBI::Replicated::Pool',
66 required=>1,
cb6ec758 67);
68
69=head2 current_replicant
70
71Replicant storages (slaves) handle all read only traffic. The assumption is
72that your database will become readbound well before it becomes write bound
73and that being able to spread your read only traffic around to multiple
74databases is going to help you to scale traffic.
75
76This attribute returns the next slave to handle a read request. Your L</pool>
77attribute has methods to help you shuffle through all the available replicants
faaba25f 78via its balancer object.
cb6ec758 79
80=cut
81
82has 'current_replicant' => (
64cdad22 83 is=> 'rw',
6a151f58 84 isa=>DBICStorageDBI,
64cdad22 85 lazy_build=>1,
86 handles=>[qw/
87 select
88 select_single
89 columns_info_for
90 /],
cb6ec758 91);
92
26ab719a 93=head1 METHODS
94
95This class defines the following methods.
96
cb6ec758 97=head2 _build_current_replicant
98
99Lazy builder for the L</current_replicant_storage> attribute.
100
101=cut
102
103sub _build_current_replicant {
64cdad22 104 my $self = shift @_;
105 $self->next_storage;
cb6ec758 106}
107
108=head2 next_storage
26ab719a 109
17b05c13 110This method should be defined in the class which consumes this role.
111
26ab719a 112Given a pool object, return the next replicant that will serve queries. The
48580715 113default behavior is to grab the first replicant it finds but you can write
cb6ec758 114your own subclasses of L<DBIx::Class::Storage::DBI::Replicated::Balancer> to
115support other balance systems.
26ab719a 116
106d5f3b 117This returns from the pool of active replicants. If there are no active
118replicants, then you should have it return the master as an ultimate fallback.
119
17b05c13 120=head2 around: next_storage
121
122Advice on next storage to add the autovalidation. We have this broken out so
123that it's easier to break out the auto validation into a role.
124
125This also returns the master in the case that none of the replicants are active
126or just just forgot to create them :)
7edf5f1c 127
26ab719a 128=cut
129
8592e2d1 130my $on_master;
131
17b05c13 132around 'next_storage' => sub {
64cdad22 133 my ($next_storage, $self, @args) = @_;
134 my $now = time;
13b9e828 135
64cdad22 136 ## Do we need to validate the replicants?
137 if(
138 $self->has_auto_validate_every &&
139 ($self->auto_validate_every + $self->pool->last_validated) <= $now
13b9e828 140 ) {
64cdad22 141 $self->pool->validate_replicants;
142 }
13b9e828 143
64cdad22 144 ## Get a replicant, or the master if none
bc376f59 145 if(my $next = $self->$next_storage(@args)) {
8592e2d1 146 $self->master->debugobj->print("Moved back to slave\n") if $on_master;
147 $on_master = 0;
bc376f59 148 return $next;
149 } else {
8592e2d1 150 $self->master->debugobj->print("No Replicants validate, falling back to master reads.\n")
151 unless $on_master++;
152
bc376f59 153 return $self->master;
154 }
17b05c13 155};
26ab719a 156
bbafcf26 157=head2 increment_storage
cb6ec758 158
bbafcf26 159Rolls the Storage to whatever is next in the queue, as defined by the Balancer.
cb6ec758 160
161=cut
162
bbafcf26 163sub increment_storage {
64cdad22 164 my $self = shift @_;
165 my $next_replicant = $self->next_storage;
166 $self->current_replicant($next_replicant);
bbafcf26 167}
168
169=head2 around: select
170
171Advice on the select attribute. Each time we use a replicant
172we need to change it via the storage pool algorithm. That way we are spreading
173the load evenly (hopefully) across existing capacity.
174
175=cut
176
177around 'select' => sub {
178 my ($select, $self, @args) = @_;
bd5da369 179
7e38d850 180 if (my $forced_pool = $args[-1]->{force_pool}) {
181 delete $args[-1]->{force_pool};
182 return $self->_get_forced_pool($forced_pool)->select(@args);
bd5da369 183 } elsif($self->master->{transaction_depth}) {
184 return $self->master->select(@args);
bbafcf26 185 } else {
186 $self->increment_storage;
187 return $self->$select(@args);
188 }
cb6ec758 189};
190
bbafcf26 191=head2 around: select_single
cb6ec758 192
193Advice on the select_single attribute. Each time we use a replicant
194we need to change it via the storage pool algorithm. That way we are spreading
195the load evenly (hopefully) across existing capacity.
196
197=cut
198
bbafcf26 199around 'select_single' => sub {
200 my ($select_single, $self, @args) = @_;
bd5da369 201
7e38d850 202 if (my $forced_pool = $args[-1]->{force_pool}) {
bc376f59 203 delete $args[-1]->{force_pool};
204 return $self->_get_forced_pool($forced_pool)->select_single(@args);
bd5da369 205 } elsif($self->master->{transaction_depth}) {
206 return $self->master->select_single(@args);
bbafcf26 207 } else {
bc376f59 208 $self->increment_storage;
bbafcf26 209 return $self->$select_single(@args);
210 }
cb6ec758 211};
212
106d5f3b 213=head2 before: columns_info_for
cb6ec758 214
215Advice on the current_replicant_storage attribute. Each time we use a replicant
216we need to change it via the storage pool algorithm. That way we are spreading
217the load evenly (hopefully) across existing capacity.
218
219=cut
220
106d5f3b 221before 'columns_info_for' => sub {
64cdad22 222 my $self = shift @_;
bbafcf26 223 $self->increment_storage;
cb6ec758 224};
26ab719a 225
7e38d850 226=head2 _get_forced_pool ($name)
227
228Given an identifier, find the most correct storage object to handle the query.
229
230=cut
231
232sub _get_forced_pool {
233 my ($self, $forced_pool) = @_;
234 if(blessed $forced_pool) {
235 return $forced_pool;
236 } elsif($forced_pool eq 'master') {
237 return $self->master;
bd5da369 238 } elsif(my $replicant = $self->pool->replicants->{$forced_pool}) {
7e38d850 239 return $replicant;
240 } else {
241 $self->master->throw_exception("$forced_pool is not a named replicant.");
242 }
243}
244
26ab719a 245=head1 AUTHOR
246
bd5da369 247John Napiorkowski <jjnapiork@cpan.org>
26ab719a 248
249=head1 LICENSE
250
251You may distribute this code under the same terms as Perl itself.
252
253=cut
254
cb6ec758 2551;