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