File Coverage

blib/lib/DBIx/Connector/Retry.pm
Criterion Covered Total %
statement 57 64 89.0
branch 14 18 77.7
condition n/a
subroutine 11 14 78.5
pod 2 3 66.6
total 84 99 84.8


line stmt bran cond sub pod time code
1             package DBIx::Connector::Retry;
2              
3             our $AUTHORITY = 'cpan:GSG';
4             # ABSTRACT: DBIx::Connector with block retry support
5 2     2   465547 use version;
  2         3774  
  2         12  
6             our $VERSION = 'v0.900.1'; # VERSION
7              
8 2     2   197 use strict;
  2         5  
  2         40  
9 2     2   9 use warnings;
  2         9  
  2         49  
10              
11 2     2   1162 use Moo;
  2         23274  
  2         9  
12              
13             extends 'DBIx::Connector', 'Moo::Object';
14              
15 2     2   3065 use Scalar::Util qw( weaken );
  2         5  
  2         117  
16 2     2   1211 use Types::Standard qw( Str Bool HashRef CodeRef Dict Tuple Optional Maybe );
  2         152045  
  2         22  
17 2     2   4137 use Types::Common::Numeric qw( PositiveInt );
  2         25201  
  2         21  
18              
19 2     2   1957 use namespace::clean; # don't export the above
  2         24044  
  2         14  
20              
21             #pod =encoding utf8
22             #pod
23             #pod =head1 SYNOPSIS
24             #pod
25             #pod my $conn = DBIx::Connector::Retry->new(
26             #pod connect_info => [ 'dbi:Driver:database=foobar', $user, $pass, \%args ],
27             #pod retry_debug => 1,
28             #pod max_attempts => 5,
29             #pod );
30             #pod
31             #pod # Keep retrying/reconnecting on errors
32             #pod my ($count) = $conn->run(ping => sub {
33             #pod $_->do('UPDATE foobar SET updated = 1 WHERE active = ?', undef, 'on');
34             #pod $_->selectrow_array('SELECT COUNT(*) FROM foobar WHERE updated = 1');
35             #pod });
36             #pod
37             #pod # Add a simple retry_handler for a manual timeout
38             #pod my $start_time = time;
39             #pod $conn->retry_handler(sub { time <= $start_time + 60 });
40             #pod
41             #pod my ($count) = $conn->txn(fixup => sub {
42             #pod $_->selectrow_array('SELECT COUNT(*) FROM barbaz');
43             #pod });
44             #pod $conn->clear_retry_handler;
45             #pod
46             #pod # Plus everything else in DBIx::Connector
47             #pod
48             #pod =head1 DESCRIPTION
49             #pod
50             #pod DBIx::Connector::Retry is a Moo-based subclass of L that will retry on
51             #pod failures. Most of the interface was modeled after L
52             #pod and adapted for use in DBIx::Connector.
53             #pod
54             #pod =head1 ATTRIBUTES
55             #pod
56             #pod =head2 connect_info
57             #pod
58             #pod An arrayref that contains all of the connection details normally found in the L or
59             #pod L call. This data can be changed, but won't take effect until the next
60             #pod C<$dbh> re-connection cycle.
61             #pod
62             #pod Obviously, this is required.
63             #pod
64             #pod =cut
65              
66             has connect_info => (
67             is => 'rw',
68             # Yes, DBI->connect() is still technically-valid syntax
69             isa => Tuple[ Maybe[Str], Maybe[Str], Maybe[Str], Optional[HashRef] ],
70             required => 1,
71             );
72              
73             #pod =head2 mode
74             #pod
75             #pod This is just like L except that it can be set from within the
76             #pod constructor.
77             #pod
78             #pod Unlike DBIx::Connector, the default is C, not C.
79             #pod
80             #pod =cut
81              
82             has _mode => (
83             is => 'bare', # use DBIx::Connector's accessor
84             isa => Str,
85             init_arg => 'mode',
86             required => 0,
87             default => 'ping',
88             );
89              
90             #pod =head2 disconnect_on_destroy
91             #pod
92             #pod This is just like L except that it can be set
93             #pod from within the constructor.
94             #pod
95             #pod Default is on.
96             #pod
97             #pod =cut
98              
99             has _dond => (
100             is => 'bare', # use DBIx::Connector's accessor
101             isa => Bool,
102             init_arg => 'disconnect_on_destroy',
103             required => 0,
104             default => 1,
105             );
106              
107             #pod =head2 max_attempts
108             #pod
109             #pod The maximum amount of block running attempts before the Connector gives up and dies.
110             #pod
111             #pod Default is 10.
112             #pod
113             #pod =cut
114              
115             has max_attempts => (
116             is => 'rw',
117             isa => PositiveInt,
118             required => 0,
119             default => 10,
120             );
121              
122             #pod =head2 retry_debug
123             #pod
124             #pod If enabled, any retries will output a debug warning with the error message and number
125             #pod of retries.
126             #pod
127             #pod =cut
128              
129             has retry_debug => (
130             is => 'rw',
131             isa => Bool,
132             required => 0,
133             default => 0,
134             lazy => 1,
135             );
136              
137             #pod =head2 retry_handler
138             #pod
139             #pod An optional handler that will be checked on each retry. It will be passed the Connector
140             #pod object as its only input. If the handler returns a true value, retries will continue.
141             #pod A false value will cause the retry loop to immediately rethrow the exception. You can
142             #pod also throw your own, if you prefer.
143             #pod
144             #pod This check is independent of checks for L.
145             #pod
146             #pod The last exception can be inspected as part of the check by looking at L.
147             #pod This is recommended to make sure the failure is actually what you expect it to be.
148             #pod For example:
149             #pod
150             #pod $conn->retry_handler(sub {
151             #pod my $c = shift;
152             #pod my $err = $c->last_exception;
153             #pod $err = $err->error if blessed $err && $err->isa('DBIx::Connector::RollbackError');
154             #pod
155             #pod $err =~ /deadlock|timeout/i; # only retry on deadlocks or timeouts
156             #pod });
157             #pod
158             #pod Default is an always-true coderef.
159             #pod
160             #pod This attribute has the following handles:
161             #pod
162             #pod =head3 clear_retry_handler
163             #pod
164             #pod Sets it back to the always-true default.
165             #pod
166             #pod =cut
167              
168             has retry_handler => (
169             is => 'rw',
170             isa => CodeRef,
171             required => 1,
172             default => sub { sub { 1 } },
173             );
174              
175 0     0 1 0 sub clear_retry_handler { shift->retry_handler(sub { 1 }) }
  0     0   0  
176              
177             #pod =head2 failed_attempt_count
178             #pod
179             #pod The number of failed attempts so far. This can be used in the L or
180             #pod checked afterwards. It will be reset on each block run.
181             #pod
182             #pod Not available for initialization.
183             #pod
184             #pod =cut
185              
186             has failed_attempt_count => (
187             is => 'ro',
188             init_arg => undef,
189             writer => '_set_failed_attempt_count',
190             default => 0,
191             lazy => 1,
192             trigger => sub {
193             my ($self, $val) = @_;
194             die sprintf (
195             'Reached max_attempts amount of %d, latest exception: %s',
196             $self->max_attempts, $self->last_exception
197             ) if $self->max_attempts <= ( $val || 0 );
198             },
199             );
200              
201             #pod =head2 exception_stack
202             #pod
203             #pod The stack of exceptions received so far, as an arrayref. This can be used in the
204             #pod L or checked afterwards. It will be reset on each block run.
205             #pod
206             #pod Not available for initialization.
207             #pod
208             #pod This attribute has the following handles:
209             #pod
210             #pod =head3 last_exception
211             #pod
212             #pod The last exception on the stack.
213             #pod
214             #pod =cut
215              
216             has exception_stack => (
217             is => 'ro',
218             init_arg => undef,
219             clearer => '_reset_exception_stack',
220             default => sub { [] },
221             lazy => 1,
222             );
223              
224 0     0 1 0 sub last_exception { shift->exception_stack->[-1] }
225              
226             #pod =head1 CONSTRUCTORS
227             #pod
228             #pod =head2 new
229             #pod
230             #pod my $conn = DBIx::Connector::Retry->new(
231             #pod connect_info => [ 'dbi:Driver:database=foobar', $user, $pass, \%args ],
232             #pod max_attempts => 5,
233             #pod # ...etc...
234             #pod );
235             #pod
236             #pod # Old-DBI syntax
237             #pod my $conn = DBIx::Connector::Retry->new(
238             #pod 'dbi:Driver:database=foobar', $user, $pass, \%dbi_args,
239             #pod max_attempts => 5,
240             #pod # ...etc...
241             #pod );
242             #pod
243             #pod As this is a L class, it uses the standard Moo constructor. The L
244             #pod should be specified as its own key. The L/L syntax is available,
245             #pod but only as a nicety for compatibility.
246             #pod
247             #pod =cut
248              
249             around BUILDARGS => sub {
250             my ($orig, $class, @args) = @_;
251              
252             # Old-style DBI/DBIx::Connector parameters. Try to fix it up.
253             if (@args && $args[0] && !ref $args[0] && $args[0] =~ /^dbi:/) {
254             my @connect_info = splice(@args, 0, 3); # DBI DSN, UN, PW
255             push @connect_info, shift @args if $args[0] && (ref $args[0]||'') eq 'HASH'; # DBI \%attr, if it exists
256              
257             if ( @args && $args[0] && (my $ref = ref $args[0]) ) {
258             if ($ref eq 'ARRAY') {
259             push @{$args[0]}, ( connect_info => \@connect_info );
260             @args = @{$args[0]}; # Moo::Object::BUILDARGS doesn't actually support lone ARRAYREFs
261             }
262             elsif ($ref eq 'HASH') {
263             $args[0]{connect_info} = \@connect_info;
264             }
265             else {
266             # Mimicing Moo::Object::BUILDARGS here
267             Carp::croak(join ' ',
268             "The new() method for $class cannot parse the strange argument list.",
269             "Please switch to a standard Moo constructor, instead of the DBI syntax.",
270             );
271             }
272             }
273             else {
274             # either the key within a list or we're out of arguments
275             push @args, ( connect_info => \@connect_info );
276             }
277             }
278              
279             return $class->$orig(@args);
280             };
281              
282             sub BUILD {
283 12     12 0 1085 my ($self, $args) = @_;
284              
285 12         24 my @connect_args = @{ $self->connect_info };
  12         213  
286              
287             # Add in the keys that DBIx::Connector expects. For the purposes of future
288             # expandability of DBIx::Connector, we do this by getting a new base Connector
289             # object, and inject those properties into our own object.
290              
291 12         122 my $base_obj = DBIx::Connector->new(@connect_args);
292 12         236 %$self = (
293             %$base_obj,
294             %$self, # $self's existing attributes take priority
295             );
296              
297             # DBIx::Connector stores connection details in a coderef (for some reason). Instead
298             # of just dumping the same arguments as another copy, we'll tie it directly to the
299             # attr. If connect_info ever changes, it will grab the latest version.
300 12     8   76 $self->{_args} = sub { @{ $self->connect_info } };
  8         301  
  8         155  
301 12         62 weaken $self; # circular closure ref
302             }
303              
304             #pod =head1 MODIFIED METHODS
305             #pod
306             #pod =head2 run / txn
307             #pod
308             #pod my @result = $conn->run($mode => $coderef);
309             #pod my $result = $conn->run($mode => $coderef);
310             #pod $conn->run($mode => $coderef);
311             #pod
312             #pod my @result = $conn->txn($mode => $coderef);
313             #pod my $result = $conn->txn($mode => $coderef);
314             #pod $conn->txn($mode => $coderef);
315             #pod
316             #pod Both L and L are modified to run inside
317             #pod a retry loop. If the original Connector action dies, the exception is caught, and if
318             #pod L and L allows it, the action is retried. The database
319             #pod handle may be reset by the Connector action, according to its connection mode.
320             #pod
321             #pod See L for important behaviors/limitations.
322             #pod
323             #pod =cut
324              
325             foreach my $method (qw< run txn >) {
326             around $method => sub {
327             my $orig = shift;
328             my $self = shift;
329             my $mode = ref $_[0] eq 'CODE' ? $self->{_mode} : shift;
330             my $cref = shift;
331              
332             my $wantarray = wantarray;
333              
334             return $self->_retry_loop($orig, $method, $mode, $cref, $wantarray);
335             };
336             }
337              
338             sub _retry_loop {
339 35     35   101 my ($self, $orig, $method, $mode, $cref, $wantarray) = @_;
340              
341 35         614 $self->_reset_exception_stack;
342 35         750 $self->_set_failed_attempt_count(0);
343              
344             # If we already started in a transaction, that implies nesting, so don't
345             # retry the query. We can't guarantee that the statements before the block
346             # run will be committed, and are assuming that the connection will break.
347             #
348             # We cannot rely on checking the database connection via ping, because some
349             # DBDs (like mysql) will try to reconnect to the DB if the first ping check
350             # fails, and a reconnection auto-rollbacks all transactions, locks, etc.
351 35 100       462 if ($self->in_txn) {
352 25 50       237 unless (defined $wantarray) { return $self->$orig($mode, $cref) }
  0 50       0  
353 0         0 elsif ($wantarray) { return ($self->$orig($mode, $cref)) }
  25         59  
354 0         0 else { return scalar $self->$orig($mode, $cref) }
355             }
356              
357             # Mode is localized within $orig, but we should localize it again ourselves, in case
358             # it's changed on-the-fly.
359 10         153 local $self->{_mode} = $mode;
360              
361 10         20 my $run_err;
362             my @res;
363              
364 10         22 do {
365             TRY: {
366 34         215 local $@;
  34         56  
367 34         61 eval {
368 34 100       74 unless (defined $wantarray) { $self->$orig($mode, $cref) }
  32 100       101  
369 0         0 elsif ($wantarray) { @res = $self->$orig($mode, $cref) }
  1         8  
370 1         4 else { $res[0] = $self->$orig($mode, $cref) }
371             };
372 34         762998 $run_err = $@;
373             }
374              
375 34 100       185 if ($run_err) {
376 24         66 push @{ $self->exception_stack }, $run_err;
  24         489  
377              
378             # This will throw if max_attempts is reached
379 24         405 $self->_set_failed_attempt_count($self->failed_attempt_count + 1);
380              
381             # If the retry handler says no, then die
382 24 50       527 die $run_err unless $self->retry_handler->($self);
383              
384             # Debug line
385 24 50       548 warn sprintf(
386             'Retrying %s coderef (attempt %d) after caught exception: %s',
387             $method,
388             $self->failed_attempt_count + 1,
389             $run_err,
390             ) if $self->retry_debug;
391             }
392             } while ($run_err);
393              
394 10 100       97 return $wantarray ? @res : $res[0];
395             }
396              
397             #pod =head1 CAVEATS
398             #pod
399             #pod =head2 $dbh settings
400             #pod
401             #pod Like L, it's important that the L properties have sane
402             #pod connection settings.
403             #pod
404             #pod L should be turned on. Otherwise, the connection is
405             #pod considered to be already in a transaction, and no retries will be attempted. Instead,
406             #pod use transactions via L.
407             #pod
408             #pod L should also be turned on, since exceptions are captured,
409             #pod and both Retry and Connector use them to determine if any of the C<$dbh> calls failed.
410             #pod
411             #pod =head2 Savepoints and nested transactions
412             #pod
413             #pod L is NOT modified to work inside of a retry loop,
414             #pod because retries are generally not possible for savepoints, and a disconnected connection
415             #pod will rollback any uncommited statements in most RDBMS. The same goes for any C/C
416             #pod calls attempted inside of a transaction.
417             #pod
418             #pod Consider the following:
419             #pod
420             #pod # If this dies, sub will retry
421             #pod $conn->txn(ping => sub {
422             #pod shift->do('UPDATE foobar SET updated = 1 WHERE active = ?', undef, 'on');
423             #pod
424             #pod # If this dies, it will not retry
425             #pod $conn->svp(sub {
426             #pod my $c = shift;
427             #pod $c->do('INSERT foobar (name, updated, active) VALUES (?, ?)', undef, 'barbaz', 0, 'off');
428             #pod });
429             #pod });
430             #pod
431             #pod If the savepoint actually tried to retry, the C statement would get rolled back by
432             #pod virtue of database disconnection. However, the savepoint code would continue, possibly
433             #pod even succeeding. You would never know that the C statement was rolled back.
434             #pod
435             #pod However, without savepoint retry support, as it is currently designed, the statements
436             #pod will work as expected. If the savepoint code dies, and if C<$conn> is set up for
437             #pod retries, the transaction code is restarted, after a rollback or reconnection. Thus, the
438             #pod C and C statements are both ran properly if they now succeed.
439             #pod
440             #pod Obviously, this will not work if transactions are manually started outside of the main
441             #pod Connector interface:
442             #pod
443             #pod # Don't do this! The whole transaction isn't compartmentalized properly!
444             #pod $conn->run(ping => sub {
445             #pod $_->begin_work; # don't ever call this!
446             #pod $_->do('UPDATE foobar SET updated = 1 WHERE active = ?', undef, 'on');
447             #pod });
448             #pod
449             #pod # If this dies, the whole app will probably crash
450             #pod $conn->svp(sub {
451             #pod my $c = shift;
452             #pod $c->do('INSERT foobar (name, updated, active) VALUES (?, ?)', undef, 'barbaz', 0, 'off');
453             #pod });
454             #pod
455             #pod # Don't do this!
456             #pod $conn->run(ping => sub {
457             #pod $_->commit; # no, let Connector handle this process!
458             #pod });
459             #pod
460             #pod =head2 Fixup mode
461             #pod
462             #pod Because of the nature of L, the block may be
463             #pod executed twice as often. Functionally, the code looks like this:
464             #pod
465             #pod # Very simplified example
466             #pod sub fixup_run {
467             #pod my ($self, $code) = @_;
468             #pod
469             #pod my (@ret, $run_err);
470             #pod do {
471             #pod eval {
472             #pod @ret = eval { $code->($dbh) };
473             #pod my $err = $@;
474             #pod
475             #pod if ($err) {
476             #pod die $err if $self->connected;
477             #pod # Not connected. Try again.
478             #pod return $code->($dbh);
479             #pod }
480             #pod };
481             #pod $run_err = $@;
482             #pod
483             #pod if ($run_err) {
484             #pod # Push exception_stack, set/check attempts, check retry_handler
485             #pod }
486             #pod } while ($run_err);
487             #pod return @ret;
488             #pod }
489             #pod
490             #pod If the first eval dies because of a connection failure, the code is ran twice before the
491             #pod retry loop finds it. This is only considered to be one attempt. If it dies because of
492             #pod some other fault, it's only ran once and continues the retry loop.
493             #pod
494             #pod If this is behavior is undesirable, this can be worked around by using the L
495             #pod to change the L after the first attempt:
496             #pod
497             #pod $conn->retry_handler(sub {
498             #pod my $c = shift;
499             #pod $c->mode('ping') if $c->mode eq 'fixup';
500             #pod 1;
501             #pod });
502             #pod
503             #pod Mode is localized outside of the retry loop, so even C<< $conn->run(fixup => $code) >>
504             #pod calls work, and the default mode will return to normal after the block run.
505             #pod
506             #pod =head1 SEE ALSO
507             #pod
508             #pod L, L
509             #pod
510             #pod =cut
511              
512             1;
513              
514             __END__