Port ::Replicated from Moose to Moo+Type::Tiny
[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 use Types::Standard qw/Int/;
6 use Type::Utils qw/class_type/;
7 use Scalar::Util qw/blessed/;
8 use DBIx::Class::Storage::DBI::Replicated::Pool;
9 use DBIx::Class::Storage::DBI::Replicated::Types qw/DBICStorageDBI/;
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=>Int,
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=>class_type('DBIx::Class::Storage::DBI::Replicated::Pool'),
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   predicate=>1,
91   clearer=>1,
92   handles=>[qw/
93     select
94     select_single
95     columns_info_for
96   /],
97 );
98
99 =head1 METHODS
100
101 This class defines the following methods.
102
103 =head2 _build_current_replicant
104
105 Lazy builder for the L</current_replicant> attribute.
106
107 =cut
108
109 sub _build_current_replicant {
110   my $self = shift @_;
111   $self->next_storage;
112 }
113
114 =head2 next_storage
115
116 This method should be defined in the class which consumes this role.
117
118 Given a pool object, return the next replicant that will serve queries.  The
119 default behavior is to grab the first replicant it finds but you can write
120 your own subclasses of L<DBIx::Class::Storage::DBI::Replicated::Balancer> to
121 support other balance systems.
122
123 This returns from the pool of active replicants.  If there are no active
124 replicants, then you should have it return the master as an ultimate fallback.
125
126 =head2 around: next_storage
127
128 Advice on next storage to add the autovalidation.  We have this broken out so
129 that it's easier to break out the auto validation into a role.
130
131 This also returns the master in the case that none of the replicants are active
132 or just forgot to create them :)
133
134 =cut
135
136 my $on_master;
137
138 around 'next_storage' => sub {
139   my ($next_storage, $self, @args) = @_;
140   my $now = time;
141
142   ## Do we need to validate the replicants?
143   if(
144      $self->has_auto_validate_every &&
145      ($self->auto_validate_every + $self->pool->last_validated) <= $now
146   ) {
147       $self->pool->validate_replicants;
148   }
149
150   ## Get a replicant, or the master if none
151   if(my $next = $self->$next_storage(@args)) {
152     $self->master->debugobj->print("Moved back to slave\n") if $on_master;
153     $on_master = 0;
154     return $next;
155   } else {
156     $self->master->debugobj->print("No Replicants validate, falling back to master reads.\n")
157        unless $on_master++;
158
159     return $self->master;
160   }
161 };
162
163 =head2 increment_storage
164
165 Rolls the Storage to whatever is next in the queue, as defined by the Balancer.
166
167 =cut
168
169 sub increment_storage {
170   my $self = shift @_;
171   my $next_replicant = $self->next_storage;
172   $self->current_replicant($next_replicant);
173 }
174
175 =head2 around: select
176
177 Advice on the select attribute.  Each time we use a replicant
178 we need to change it via the storage pool algorithm.  That way we are spreading
179 the load evenly (hopefully) across existing capacity.
180
181 =cut
182
183 around 'select' => sub {
184   my ($select, $self, @args) = @_;
185
186   if (my $forced_pool = $args[-1]->{force_pool}) {
187     delete $args[-1]->{force_pool};
188     return $self->_get_forced_pool($forced_pool)->select(@args);
189   } elsif($self->master->{transaction_depth}) {
190     return $self->master->select(@args);
191   } else {
192     $self->increment_storage;
193     return $self->$select(@args);
194   }
195 };
196
197 =head2 around: select_single
198
199 Advice on the select_single attribute.  Each time we use a replicant
200 we need to change it via the storage pool algorithm.  That way we are spreading
201 the load evenly (hopefully) across existing capacity.
202
203 =cut
204
205 around 'select_single' => sub {
206   my ($select_single, $self, @args) = @_;
207
208   if (my $forced_pool = $args[-1]->{force_pool}) {
209     delete $args[-1]->{force_pool};
210     return $self->_get_forced_pool($forced_pool)->select_single(@args);
211   } elsif($self->master->{transaction_depth}) {
212     return $self->master->select_single(@args);
213   } else {
214     $self->increment_storage;
215     return $self->$select_single(@args);
216   }
217 };
218
219 =head2 before: columns_info_for
220
221 Advice on the current_replicant_storage attribute.  Each time we use a replicant
222 we need to change it via the storage pool algorithm.  That way we are spreading
223 the load evenly (hopefully) across existing capacity.
224
225 =cut
226
227 before 'columns_info_for' => sub {
228   my $self = shift @_;
229   $self->increment_storage;
230 };
231
232 =head2 _get_forced_pool ($name)
233
234 Given an identifier, find the most correct storage object to handle the query.
235
236 =cut
237
238 sub _get_forced_pool {
239   my ($self, $forced_pool) = @_;
240   if(blessed $forced_pool) {
241     return $forced_pool;
242   } elsif($forced_pool eq 'master') {
243     return $self->master;
244   } elsif(my $replicant = $self->pool->replicants->{$forced_pool}) {
245     return $replicant;
246   } else {
247     $self->master->throw_exception("'$forced_pool' is not a named replicant.");
248   }
249 }
250
251 =head1 FURTHER QUESTIONS?
252
253 Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
254
255 =head1 COPYRIGHT AND LICENSE
256
257 This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
258 by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
259 redistribute it and/or modify it under the same terms as the
260 L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
261
262 =cut
263
264 1;