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