Port replicated storage from MXAH to native traits.
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBI / Replicated / Pool.pm
CommitLineData
26ab719a 1package DBIx::Class::Storage::DBI::Replicated::Pool;
2
3use Moose;
26ab719a 4use DBIx::Class::Storage::DBI::Replicated::Replicant;
9901aad7 5use List::Util 'sum';
6use Scalar::Util 'reftype';
0bd8e058 7use DBI ();
9901aad7 8use Carp::Clan qw/^DBIx::Class/;
41916570 9use MooseX::Types::Moose qw/Num Int ClassName HashRef/;
9901aad7 10
11use namespace::clean -except => 'meta';
26ab719a 12
13=head1 NAME
14
21fc4719 15DBIx::Class::Storage::DBI::Replicated::Pool - Manage a pool of replicants
26ab719a 16
17=head1 SYNOPSIS
18
19This class is used internally by L<DBIx::Class::Storage::DBI::Replicated>. You
20shouldn't need to create instances of this class.
d4daee7b 21
26ab719a 22=head1 DESCRIPTION
23
24In a replicated storage type, there is at least one replicant to handle the
25read only traffic. The Pool class manages this replicant, or list of
26replicants, and gives some methods for querying information about their status.
27
28=head1 ATTRIBUTES
29
30This class defines the following attributes.
31
4a607d7a 32=head2 maximum_lag ($num)
33
34This is a number which defines the maximum allowed lag returned by the
35L<DBIx::Class::Storage::DBI/lag_behind_master> method. The default is 0. In
36general, this should return a larger number when the replicant is lagging
faaba25f 37behind its master, however the implementation of this is database specific, so
4a607d7a 38don't count on this number having a fixed meaning. For example, MySQL will
39return a number of seconds that the replicating database is lagging.
40
41=cut
42
43has 'maximum_lag' => (
64cdad22 44 is=>'rw',
41916570 45 isa=>Num,
64cdad22 46 required=>1,
47 lazy=>1,
48 default=>0,
4a607d7a 49);
50
17b05c13 51=head2 last_validated
52
53This is an integer representing a time since the last time the replicants were
faaba25f 54validated. It's nothing fancy, just an integer provided via the perl L<time|perlfunc/time>
17b05c13 55builtin.
56
57=cut
58
59has 'last_validated' => (
64cdad22 60 is=>'rw',
41916570 61 isa=>Int,
64cdad22 62 reader=>'last_validated',
63 writer=>'_last_validated',
64 lazy=>1,
65 default=>0,
17b05c13 66);
67
4a607d7a 68=head2 replicant_type ($classname)
26ab719a 69
70Base class used to instantiate replicants that are in the pool. Unless you
71need to subclass L<DBIx::Class::Storage::DBI::Replicated::Replicant> you should
72just leave this alone.
73
74=cut
75
76has 'replicant_type' => (
64cdad22 77 is=>'ro',
41916570 78 isa=>ClassName,
64cdad22 79 required=>1,
80 default=>'DBIx::Class::Storage::DBI',
81 handles=>{
82 'create_replicant' => 'new',
83 },
26ab719a 84);
85
26ab719a 86=head2 replicants
87
88A hashref of replicant, with the key being the dsn and the value returning the
89actual replicant storage. For example if the $dsn element is something like:
90
64cdad22 91 "dbi:SQLite:dbname=dbfile"
d4daee7b 92
26ab719a 93You could access the specific replicant via:
94
64cdad22 95 $schema->storage->replicants->{'dbname=dbfile'}
d4daee7b 96
64cdad22 97This attributes also supports the following helper methods:
26ab719a 98
99=over 4
100
101=item set_replicant($key=>$storage)
102
103Pushes a replicant onto the HashRef under $key
104
105=item get_replicant($key)
106
107Retrieves the named replicant
108
109=item has_replicants
110
111Returns true if the Pool defines replicants.
112
113=item num_replicants
114
115The number of replicants in the pool
116
117=item delete_replicant ($key)
118
119removes the replicant under $key from the pool
120
121=back
122
123=cut
124
125has 'replicants' => (
64cdad22 126 is=>'rw',
c4d78acb 127 traits => ['Hash'],
071bbccb 128 isa=>HashRef['Object'],
64cdad22 129 default=>sub {{}},
c4d78acb 130 handles => {
131 'set_replicant' => 'set',
132 'get_replicant' => 'get',
133 'has_replicants' => 'is_empty',
134 'num_replicants' => 'count',
135 'delete_replicant' => 'delete',
136 'all_replicant_storages' => 'values',
64cdad22 137 },
26ab719a 138);
139
c4d78acb 140around has_replicants => sub {
141 my ($orig, $self) = @_;
142 return !$self->$orig;
143};
144
ede99b9f 145has next_unknown_replicant_id => (
146 is => 'rw',
c4d78acb 147 traits => ['Counter'],
ede99b9f 148 isa => Int,
149 default => 1,
c4d78acb 150 handles => {
151 'inc_unknown_replicant_id' => 'inc',
ede99b9f 152 },
153);
154
26ab719a 155=head1 METHODS
156
157This class defines the following methods.
158
955a6df6 159=head2 connect_replicants ($schema, Array[$connect_info])
26ab719a 160
d40080c3 161Given an array of $dsn or connect_info structures suitable for connected to a
162database, create an L<DBIx::Class::Storage::DBI::Replicated::Replicant> object
163and store it in the L</replicants> attribute.
26ab719a 164
165=cut
166
955a6df6 167sub connect_replicants {
64cdad22 168 my $self = shift @_;
169 my $schema = shift @_;
d4daee7b 170
64cdad22 171 my @newly_created = ();
172 foreach my $connect_info (@_) {
2cd3ccc4 173 $connect_info = [ $connect_info ]
9901aad7 174 if reftype $connect_info ne 'ARRAY';
175
0bd8e058 176 my $connect_coderef =
177 (reftype($connect_info->[0])||'') eq 'CODE' ? $connect_info->[0]
178 : (reftype($connect_info->[0])||'') eq 'HASH' &&
179 $connect_info->[0]->{dbh_maker};
180
181 my $dsn;
4c91f824 182 my $replicant = do {
ede99b9f 183# yes this is evil, but it only usually happens once (for coderefs)
184# this will fail if the coderef does not actually DBI::connect
0bd8e058 185 no warnings 'redefine';
186 my $connect = \&DBI::connect;
187 local *DBI::connect = sub {
188 $dsn = $_[1];
189 goto $connect;
190 };
4c91f824 191 $self->connect_replicant($schema, $connect_info);
192 };
193
ede99b9f 194 my $key;
195
196 if (!$dsn) {
197 if (!$connect_coderef) {
198 $dsn = $connect_info->[0];
199 $dsn = $dsn->{dsn} if (reftype($dsn)||'') eq 'HASH';
200 }
201 else {
202 # all attempts to get the DSN failed
203 $key = "UNKNOWN_" . $self->next_unknown_replicant_id;
204 $self->inc_unknown_replicant_id;
205 }
206 }
207 if ($dsn) {
208 $replicant->dsn($dsn);
209 ($key) = ($dsn =~ m/^dbi\:.+\:(.+)$/i);
0bd8e058 210 }
0bd8e058 211
ede99b9f 212 $replicant->id($key);
0bd8e058 213 $self->set_replicant($key => $replicant);
ede99b9f 214
64cdad22 215 push @newly_created, $replicant;
216 }
d4daee7b 217
64cdad22 218 return @newly_created;
26ab719a 219}
220
bbafcf26 221=head2 connect_replicant ($schema, $connect_info)
222
223Given a schema object and a hashref of $connect_info, connect the replicant
224and return it.
225
226=cut
227
228sub connect_replicant {
229 my ($self, $schema, $connect_info) = @_;
230 my $replicant = $self->create_replicant($schema);
f15afa13 231 $replicant->connect_info($connect_info);
d40080c3 232
233## It is undesirable for catalyst to connect at ->conect_replicants time, as
234## connections should only happen on the first request that uses the database.
235## So we try to set the driver without connecting, however this doesn't always
236## work, as a driver may need to connect to determine the DB version, and this
237## may fail.
d6e80959 238##
239## Why this is necessary at all, is that we need to have the final storage
240## class to apply the Replicant role.
d40080c3 241
242 $self->_safely($replicant, '->_determine_driver', sub {
243 $replicant->_determine_driver
244 });
245
f15afa13 246 DBIx::Class::Storage::DBI::Replicated::Replicant->meta->apply($replicant);
bbafcf26 247 return $replicant;
248}
249
f15afa13 250=head2 _safely_ensure_connected ($replicant)
251
252The standard ensure_connected method with throw an exception should it fail to
253connect. For the master database this is desirable, but since replicants are
254allowed to fail, this behavior is not desirable. This method wraps the call
255to ensure_connected in an eval in order to catch any generated errors. That
d40080c3 256way a slave can go completely offline (ie, the box itself can die) without
f15afa13 257bringing down your entire pool of databases.
258
259=cut
260
261sub _safely_ensure_connected {
262 my ($self, $replicant, @args) = @_;
d40080c3 263
264 return $self->_safely($replicant, '->ensure_connected', sub {
265 $replicant->ensure_connected(@args)
266 });
267}
268
269=head2 _safely ($replicant, $name, $code)
270
271Execute C<$code> for operation C<$name> catching any exceptions and printing an
272error message to the C<<$replicant->debugobj>>.
273
274Returns 1 on success and undef on failure.
275
276=cut
277
278sub _safely {
279 my ($self, $replicant, $name, $code) = @_;
280
6ffb5be5 281 eval {
d40080c3 282 $code->()
6ffb5be5 283 };
284 if ($@) {
13b9e828 285 $replicant
6ffb5be5 286 ->debugobj
287 ->print(
d40080c3 288 sprintf( "Exception trying to $name for replicant %s, error is %s",
6ffb5be5 289 $replicant->_dbi_connect_info->[0], $@)
13b9e828 290 );
6ffb5be5 291 return;
13b9e828 292 }
6ffb5be5 293 return 1;
f15afa13 294}
295
26ab719a 296=head2 connected_replicants
297
298Returns true if there are connected replicants. Actually is overloaded to
299return the number of replicants. So you can do stuff like:
300
64cdad22 301 if( my $num_connected = $storage->has_connected_replicants ) {
302 print "I have $num_connected connected replicants";
303 } else {
304 print "Sorry, no replicants.";
305 }
26ab719a 306
307This method will actually test that each replicant in the L</replicants> hashref
308is actually connected, try not to hit this 10 times a second.
309
310=cut
311
312sub connected_replicants {
64cdad22 313 my $self = shift @_;
314 return sum( map {
315 $_->connected ? 1:0
316 } $self->all_replicants );
26ab719a 317}
318
50336325 319=head2 active_replicants
320
321This is an array of replicants that are considered to be active in the pool.
322This does not check to see if they are connected, but if they are not, DBIC
323should automatically reconnect them for us when we hit them with a query.
324
325=cut
326
327sub active_replicants {
64cdad22 328 my $self = shift @_;
329 return ( grep {$_} map {
330 $_->active ? $_:0
331 } $self->all_replicants );
50336325 332}
333
26ab719a 334=head2 all_replicants
335
336Just a simple array of all the replicant storages. No particular order to the
337array is given, nor should any meaning be derived.
338
339=cut
340
341sub all_replicants {
64cdad22 342 my $self = shift @_;
343 return values %{$self->replicants};
26ab719a 344}
345
4a607d7a 346=head2 validate_replicants
347
348This does a check to see if 1) each replicate is connected (or reconnectable),
3492) that is ->is_replicating, and 3) that it is not exceeding the lag amount
350defined by L</maximum_lag>. Replicants that fail any of these tests are set to
351inactive, and thus removed from the replication pool.
352
353This tests L<all_replicants>, since a replicant that has been previous marked
354as inactive can be reactived should it start to pass the validation tests again.
355
356See L<DBIx::Class::Storage::DBI> for more about checking if a replicating
357connection is not following a master or is lagging.
358
359Calling this method will generate queries on the replicant databases so it is
360not recommended that you run them very often.
361
13b9e828 362This method requires that your underlying storage engine supports some sort of
363native replication mechanism. Currently only MySQL native replication is
364supported. Your patches to make other replication types work are welcomed.
365
4a607d7a 366=cut
367
368sub validate_replicants {
64cdad22 369 my $self = shift @_;
370 foreach my $replicant($self->all_replicants) {
13b9e828 371 if($self->_safely_ensure_connected($replicant)) {
372 my $is_replicating = $replicant->is_replicating;
373 unless(defined $is_replicating) {
9901aad7 374 $replicant->debugobj->print("Storage Driver ".ref($self)." Does not support the 'is_replicating' method. Assuming you are manually managing.\n");
13b9e828 375 next;
376 } else {
377 if($is_replicating) {
378 my $lag_behind_master = $replicant->lag_behind_master;
379 unless(defined $lag_behind_master) {
9901aad7 380 $replicant->debugobj->print("Storage Driver ".ref($self)." Does not support the 'lag_behind_master' method. Assuming you are manually managing.\n");
13b9e828 381 next;
382 } else {
383 if($lag_behind_master <= $self->maximum_lag) {
384 $replicant->active(1);
385 } else {
386 $replicant->active(0);
387 }
388 }
389 } else {
390 $replicant->active(0);
391 }
392 }
64cdad22 393 } else {
64cdad22 394 $replicant->active(0);
7edf5f1c 395 }
64cdad22 396 }
397 ## Mark that we completed this validation.
13b9e828 398 $self->_last_validated(time);
4a607d7a 399}
400
26ab719a 401=head1 AUTHOR
402
403John Napiorkowski <john.napiorkowski@takkle.com>
404
405=head1 LICENSE
406
407You may distribute this code under the same terms as Perl itself.
408
409=cut
410
c354902c 411__PACKAGE__->meta->make_immutable;
412
cb6ec758 4131;