File Coverage

blib/lib/DBIx/Connector/Retry/MySQL.pm
Criterion Covered Total %
statement 115 127 90.5
branch 31 42 73.8
condition 14 18 77.7
subroutine 26 29 89.6
pod 4 5 80.0
total 190 221 85.9


line stmt bran cond sub pod time code
1             package DBIx::Connector::Retry::MySQL;
2              
3             # ABSTRACT: MySQL-specific DBIx::Connector with retry support
4 2     2   360734 use version;
  2         3110  
  2         10  
5             our $VERSION = 'v1.0.1'; # VERSION
6              
7 2     2   150 use strict;
  2         4  
  2         32  
8 2     2   7 use warnings;
  2         4  
  2         39  
9              
10 2     2   907 use Moo;
  2         18471  
  2         8  
11              
12             extends 'DBIx::Connector::Retry';
13              
14 2     2   2456 use Scalar::Util qw( weaken );
  2         4  
  2         89  
15 2     2   1012 use Storable qw( dclone );
  2         5468  
  2         130  
16 2     2   937 use Types::Standard qw( Bool HashRef InstanceOf ClassName );
  2         121057  
  2         15  
17 2     2   2568 use Types::Common::Numeric qw( PositiveOrZeroNum PositiveOrZeroInt );
  2         20507  
  2         14  
18 2     2   902 use Time::HiRes qw( sleep );
  2         4  
  2         15  
19              
20 2     2   1007 use Algorithm::Backoff::RetryTimeouts;
  2         26585  
  2         72  
21 2     2   685 use DBIx::ParseError::MySQL;
  2         53555  
  2         50  
22              
23 2     2   12 use namespace::clean; # don't export the above
  2         4  
  2         13  
24              
25             #pod =head1 SYNOPSIS
26             #pod
27             #pod my $conn = DBIx::Connector::Retry::MySQL->new(
28             #pod connect_info => [ 'dbi:Driver:database=foobar', $user, $pass, \%args ],
29             #pod retry_debug => 1,
30             #pod timer_options => {
31             #pod # Default options from Algorithm::Backoff::RetryTimeouts
32             #pod max_attempts => 8,
33             #pod max_actual_duration => 50,
34             #pod jitter_factor => 0.1,
35             #pod timeout_jitter_factor => 0.1,
36             #pod adjust_timeout_factor => 0.5,
37             #pod min_adjust_timeout => 5,
38             #pod # ...among others
39             #pod },
40             #pod );
41             #pod
42             #pod # Keep retrying/reconnecting on errors
43             #pod my ($count) = $conn->run(ping => sub {
44             #pod $_->do('UPDATE foobar SET updated = 1 WHERE active = ?', undef, 'on');
45             #pod $_->selectrow_array('SELECT COUNT(*) FROM foobar WHERE updated = 1');
46             #pod });
47             #pod
48             #pod my ($count) = $conn->txn(fixup => sub {
49             #pod $_->selectrow_array('SELECT COUNT(*) FROM barbaz');
50             #pod });
51             #pod
52             #pod # Plus everything else in DBIx::Connector::Retry and DBIx::Connector
53             #pod
54             #pod =head1 DESCRIPTION
55             #pod
56             #pod DBIx::Connector::Retry::MySQL is a subclass of L that will
57             #pod explicitly retry on MySQL-specific transient error messages, as identified by
58             #pod L, using L as its retry
59             #pod algorithm. This connector should be much better at handling deadlocks, connection
60             #pod errors, and Galera node flips to ensure the transaction always goes through.
61             #pod
62             #pod It is essentially a DBIx::Connector version of L.
63             #pod
64             #pod =head1 INHERITED ATTRIBUTES
65             #pod
66             #pod This inherits all of the attributes of L:
67             #pod
68             #pod =head2 L
69             #pod
70             #pod =head2 L
71             #pod
72             #pod =head2 L
73             #pod
74             #pod =head2 max_attempts
75             #pod
76             #pod Unlike L, this is just an alias to the value in
77             #pod L.
78             #pod
79             #pod As such, it has a slightly adjusted default of 8.
80             #pod
81             #pod =cut
82              
83             sub max_attempts {
84 3     3 1 30 my $self = shift;
85 3         54 my $opts = $self->timer_options;
86              
87 3 50       32 return $opts->{max_attempts} = $_[0] if @_;
88 3   100     27 return $opts->{max_attempts} // 8;
89             }
90              
91             #pod =head2 retry_debug
92             #pod
93             #pod Like L, this turns on debug warnings for
94             #pod retries. But, this module has a bit more detail in the messages.
95             #pod
96             #pod =cut
97              
98             sub _warn_retry_debug {
99 0     0   0 my $self = shift;
100              
101 0         0 my $timer = $self->_timer;
102 0         0 my $current_attempt_count = $self->failed_attempt_count + 1;
103             my $debug_msg = sprintf(
104             'Retrying %s coderef, attempt %u of %u, timer: %.1f / %.1f sec, last exception: %s',
105             $self->execute_method,
106             $current_attempt_count, $self->max_attempts,
107             $timer->{_last_timestamp} - $timer->{_start_timestamp}, $timer->{max_actual_duration},
108 0         0 $self->last_exception
109             );
110              
111 0         0 warn $debug_msg;
112             }
113              
114             #pod =head2 retry_handler
115             #pod
116             #pod Since the whole point of the module is the retry-handling code, this attribute cannot be
117             #pod set.
118             #pod
119             #pod =cut
120              
121 23     23 1 121 sub retry_handler { \&_main_retry_handler };
122              
123             #pod =head2 failed_attempt_count
124             #pod
125             #pod Unlike L, this is just an alias to the
126             #pod value in the internal timer object.
127             #pod
128             #pod =cut
129              
130 45   50 45 1 66511260 sub failed_attempt_count { shift->_timer->{_attempts} // 0 }
131              
132             # This neuters the max_attempts trigger in failed_attempt_count, so that the main check
133             # in _main_retry_handler works as expected.
134       23     sub _set_failed_attempt_count {}
135              
136             #pod =head2 L
137             #pod
138             #pod =head1 NEW ATTRIBUTES
139             #pod
140             #pod =head2 timer_class
141             #pod
142             #pod The class used for delay and timeout setting calculations. By default, it's
143             #pod L, but you can use a sub-class of this, if you so
144             #pod choose, provided that it has a similar interface.
145             #pod
146             #pod =cut
147              
148             has timer_class => (
149             is => 'ro',
150             isa => ClassName,
151             default => 'Algorithm::Backoff::RetryTimeouts',
152             );
153              
154             #pod =head2 timer_options
155             #pod
156             #pod Controls all of the options passed to the timer constructor, using L as the
157             #pod object.
158             #pod
159             #pod =cut
160              
161             has timer_options => (
162             is => 'ro',
163             isa => HashRef,
164             default => sub { {} },
165             lazy => 1,
166             );
167              
168             has _timer => (
169             is => 'rw',
170             isa => InstanceOf['Algorithm::Backoff::RetryTimeouts'],
171             init_arg => undef,
172             builder => '_build_timer',
173             clearer => '_clear_timer',
174             lazy => 1,
175             );
176              
177             sub _build_timer {
178 54     54   207 my $self = shift;
179             return $self->timer_class->new(
180 54         156 %{ dclone $self->timer_options }
  54         803  
181             );
182             }
183              
184             has _current_timeout => (
185             is => 'rw',
186             isa => PositiveOrZeroNum,
187             init_arg => undef,
188             lazy => 1,
189             );
190              
191             sub _reset_timeout {
192 38     38   69 my $self = shift;
193              
194             # Use a temporary timer to get the first timeout value
195 38         120 my $timeout = $self->_build_timer->timeout;
196 38 100       5778 $timeout = 0 if $timeout == -1;
197 38         748 $self->_current_timeout($timeout);
198             }
199              
200             #pod =head2 aggressive_timeouts
201             #pod
202             #pod Boolean that controls whether to use some of the more aggressive, query-unfriendly
203             #pod timeouts:
204             #pod
205             #pod =over
206             #pod
207             #pod =item mysql_read_timeout
208             #pod
209             #pod Controls the timeout for all read operations. Since SQL queries in the middle of
210             #pod sending its first set of row data are still considered to be in a read operation, those
211             #pod queries could time out during those circumstances.
212             #pod
213             #pod If you're confident that you don't have any SQL statements that would take longer than
214             #pod the timeout settings (or at least returning results before that time), you can turn this
215             #pod option on. Otherwise, you may experience longer-running statements going into a retry
216             #pod death spiral until they hit the final timeout and die.
217             #pod
218             #pod =item wait_timeout
219             #pod
220             #pod Controls how long the MySQL server waits for activity from the connection before timing
221             #pod out. While most applications are going to be using the database connection pretty
222             #pod frequently, the MySQL default (8 hours) is much much longer than the mere seconds this
223             #pod engine would set it to.
224             #pod
225             #pod =back
226             #pod
227             #pod Default is off. Obviously, this setting makes no sense if C
228             #pod within L is disabled.
229             #pod
230             #pod =cut
231              
232             has aggressive_timeouts => (
233             is => 'rw',
234             isa => Bool,
235             required => 0,
236             default => 0,
237             );
238              
239             #pod =head2 retries_before_error_prefix
240             #pod
241             #pod Controls the number of retries (not tries) needed before the exception message starts
242             #pod using the statistics prefix, which looks something like this:
243             #pod
244             #pod Failed run coderef: Out of retries, attempts: 5 / 4, timer: 34.5 / 50.0 sec
245             #pod
246             #pod The default is 1, which means a failed first attempt (like a non-transient failure) will
247             #pod show a normal exception, and the second attempt will use the prefix. You can set this to
248             #pod 0 to always show the prefix, or a large number like 99 to keep the exception clean.
249             #pod
250             #pod =cut
251              
252             has retries_before_error_prefix => (
253             is => 'rw',
254             isa => PositiveOrZeroInt,
255             required => 0,
256             default => 1,
257             );
258              
259             #pod =head2 parse_error_class
260             #pod
261             #pod The class used for MySQL error parsing. By default, it's L, but
262             #pod you can use a sub-class of this, if you so choose, provided that it has a similar
263             #pod interface.
264             #pod
265             #pod =cut
266              
267             has parse_error_class => (
268             is => 'ro',
269             isa => ClassName,
270             default => 'DBIx::ParseError::MySQL',
271             );
272              
273             #pod =head2 enable_retry_handler
274             #pod
275             #pod Boolean to enable the retry handler. The default is, of course, on. This can be turned
276             #pod off to temporarily disable the retry handler.
277             #pod
278             #pod =cut
279              
280             has enable_retry_handler => (
281             is => 'rw',
282             isa => Bool,
283             required => 0,
284             default => 1,
285             );
286              
287             # Alias for backwards-compatibility
288 0     0 1 0 sub clear_retry_handler { shift->enable_retry_handler(0) }
289              
290             ### All the lifecycle and private methods
291              
292             # Force in our retry_handler
293             around BUILDARGS => sub {
294             my ($orig, $class, @args) = @_;
295              
296             my $args = $class->$orig(@args);
297             $args->{retry_handler} = \&_main_retry_handler;
298              
299             return $args;
300             };
301              
302             sub BUILD {
303 23     23 0 9688 my $self = shift;
304 23         71 $self->_reset_timeout;
305 23         963 $self->_set_dbi_connect_info;
306             }
307              
308             # Return the list of timeout strings to check
309             sub _timeout_set_list {
310 72     72   181 my ($self, $type) = @_;
311              
312 72         100 my @timeout_set;
313 72 100       231 if ($type eq 'dbi') {
    50          
314 47         126 @timeout_set = (qw< connect write >);
315 47 100       700 push @timeout_set, 'read' if $self->aggressive_timeouts;
316              
317 47         466 @timeout_set = map { "mysql_${_}_timeout" } @timeout_set;
  95         336  
318             }
319             elsif ($type eq 'session') {
320 25         83 @timeout_set = (qw< lock_wait innodb_lock_wait net_read net_write >);
321 25 50       376 push @timeout_set, 'wait' if $self->aggressive_timeouts;
322              
323 25         154 @timeout_set = map { "${_}_timeout" } @timeout_set;
  100         217  
324             }
325             else {
326 0         0 die "Unknown mysql timeout set: $type";
327             }
328              
329 72         575 return @timeout_set;
330             }
331              
332             # Set the timeouts for reconnections by inserting them into the default DBI connection
333             # attributes.
334             sub _set_dbi_connect_info {
335 50     50   116 my $self = shift;
336 50 100 100     682 return unless $self->_current_timeout && $self->enable_retry_handler;
337              
338 47         1879 my $timeout = int( $self->_current_timeout + 0.5 );
339 47   100     981 my $dbi_attr = $self->connect_info->[3] //= {};
340 47 50 33     586 return unless $dbi_attr && ref $dbi_attr eq 'HASH';
341              
342 47         129 $dbi_attr->{mysql_auto_reconnect} = 0; # do not use MySQL's own reconnector
343 47         185 $dbi_attr->{$_} = $timeout for $self->_timeout_set_list('dbi');
344             }
345              
346             # Set session timeouts for post-connection variables
347             after _connect => sub {
348             my $self = shift;
349             $self->_set_retry_session_timeouts;
350             };
351              
352             sub _set_retry_session_timeouts {
353 28     28   60 my $self = shift;
354 28 100 100     397 return unless $self->_current_timeout && $self->enable_retry_handler;
355              
356 25         895 my $timeout = int( $self->_current_timeout + 0.5 );
357              
358             # Ironically, we aren't running our own SET SESSION commands with their own
359             # retry protection, since that may lead to infinite stack recursion. Instead,
360             # put it in a basic eval, and do a quick is_transient check. If it passes,
361             # let the next _run/_retry_loop call handle it.
362              
363 25         195 local $@;
364 25         54 eval {
365             # Don't let outside handlers ruin our error checking. This expires before our
366             # 'die' statement below.
367 25         136 local $SIG{__DIE__};
368              
369 25         54 my $dbh = $self->{_dbh};
370 25 50       61 if ($dbh) {
371 25         66 $dbh->do("SET SESSION $_=$timeout") for $self->_timeout_set_list('session');
372             }
373             };
374 25 50       919 if (my $error = $@) {
375 0         0 my $parsed_error = $self->parse_error_class->new($error);
376 0 0       0 die unless $parsed_error->is_transient; # bare die for $@ propagation
377 0 0       0 warn "Encountered a recoverable error during SET SESSION timeout commands: $error" if $self->retry_debug;
378             }
379             }
380              
381             # Override fixup methods (pretend we're using no_ping mode with our own retry protections)
382             sub _fixup_run {
383 21     21   567 my ($self, $code) = @_;
384 21         85 return $self->_run($code);
385             }
386              
387             sub _txn_fixup_run {
388 0     0   0 my ($self, $code) = @_;
389 0         0 return $self->_txn_run($code);
390             }
391              
392             # Modifications of the main retry loop
393             around _retry_loop => sub {
394             my $orig = shift;
395             my $self = shift;
396             my $wantarray = $_[4]; # keep it in the parameter list
397              
398             # Start new timer
399             $self->_clear_timer;
400              
401             my $timeout = $self->_timer->timeout;
402             $timeout = 0 if $timeout == -1;
403             $self->_current_timeout($timeout);
404              
405             # Save the result in a context-sensitive manner, but reset timers before we return
406             my @res;
407             unless (defined $wantarray) { $self->$orig(@_) }
408             elsif ($wantarray) { @res = $self->$orig(@_) }
409             else { $res[0] = $self->$orig(@_) }
410              
411             $self->_reset_timers_and_timeouts;
412             return $wantarray ? @res : $res[0];
413             };
414              
415             # Our retry handler
416             sub _main_retry_handler {
417 23     23   79 my $self = shift;
418              
419 23         104 my $last_error = $self->last_exception;
420              
421             # Record the failure in the timer algorithm (prior to any checks)
422 23         829 my ($sleep_time, $new_timeout) = $self->_timer->failure;
423              
424             # Retry handler is disabled?
425 23 100       3900 $self->_reset_and_die('Retry handler disabled') unless $self->enable_retry_handler;
426              
427             # If it's not a retryable error, stop here
428 22         746 my $parsed_error = $self->parse_error_class->new($last_error);
429 22 100       5578 $self->_reset_and_die('Exception not transient') unless $parsed_error->is_transient;
430              
431             # Either stop here (because of timeout or max attempts), sleep, or don't
432 21 100       4677 if ($sleep_time == -1) { $self->_reset_and_die('Out of retries') }
  2 100       12  
433 5         6390732 elsif ($sleep_time) { sleep $sleep_time; }
434              
435 19 50       140 if ($new_timeout > 0) {
436             # Reset the connection timeouts before we connect again
437 19         614 $self->_current_timeout($new_timeout);
438 19         1370 $self->_set_dbi_connect_info;
439             }
440              
441             # Force a disconnect, but only if the connection seems to be in a broken state
442 19 100       354 unless ($parsed_error->error_type eq 'lock') {
443 6         82 local $@;
444 6         12 eval { local $SIG{__DIE__}; $self->disconnect };
  6         40  
  6         39  
445             }
446              
447 19         893 return 1;
448             }
449              
450             sub _reset_timers_and_timeouts {
451 15     15   38 my $self = shift;
452              
453             # Only reset timeouts if we have to, but check before we clear
454 15   66     56 my $needs_resetting = $self->failed_attempt_count && $self->_current_timeout;
455              
456 15         609 $self->_clear_timer;
457 15         204 $self->_reset_timeout;
458              
459 15 100       885 if ($needs_resetting) {
460 8         45 $self->_set_dbi_connect_info;
461 8         30 $self->_set_retry_session_timeouts;
462             }
463              
464 15         30 return undef;
465             }
466              
467             sub _reset_and_die {
468 4     4   240 my ($self, $fail_reason) = @_;
469              
470             # First error (by default): just pass it unaltered
471 4 100       17 die $self->last_exception if $self->failed_attempt_count <= $self->retries_before_error_prefix;
472              
473 3         127 my $timer = $self->_timer;
474             my $error = sprintf(
475             'Failed %s coderef: %s, attempts: %u / %u, timer: %.1f / %.1f sec, last exception: %s',
476             $self->execute_method, $fail_reason,
477             $self->failed_attempt_count, $self->max_attempts,
478             $timer->{_last_timestamp} - $timer->{_start_timestamp}, $timer->{max_actual_duration},
479 3         31 $self->last_exception
480             );
481              
482 3         126 $self->_reset_timers_and_timeouts;
483 3         61 die $error;
484             }
485              
486             #pod =head1 CAVEATS
487             #pod
488             #pod =head2 $dbh settings
489             #pod
490             #pod See L.
491             #pod
492             #pod =head2 Savepoints and nested transactions
493             #pod
494             #pod See L.
495             #pod
496             #pod =head2 (Ab)using $dbh directly
497             #pod
498             #pod See L.
499             #pod
500             #pod =head2 Connection modes
501             #pod
502             #pod Due to the caveats of L, C mode is changed to
503             #pod just act like C mode. However, C mode is safer to use in this module
504             #pod because it comes with the same retry protections as the other modes. Certain retries,
505             #pod such as connection/server errors, come with an explicit disconnect to make sure it starts
506             #pod back up with a clean slate.
507             #pod
508             #pod In C mode, the DB will be pinged on the first try. If the retry explicitly
509             #pod disconnected, the connector will simply connect back to the DB and run the code, without
510             #pod a superfluous ping.
511             #pod
512             #pod =cut
513              
514             1;
515              
516             __END__