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