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