File Coverage

blib/lib/DBIx/Class/Storage/DBI/mysql/Retryable.pm
Criterion Covered Total %
statement 168 201 83.5
branch 56 84 66.6
condition 16 27 59.2
subroutine 36 38 94.7
pod 4 8 50.0
total 280 358 78.2


line stmt bran cond sub pod time code
1             package DBIx::Class::Storage::DBI::mysql::Retryable;
2              
3 3     3   601658 use strict;
  3         8  
  3         127  
4 3     3   18 use warnings;
  3         8  
  3         211  
5              
6 3     3   5109 use DBI '1.630';
  3         76277  
  3         755  
7 3     3   33 use base qw< DBIx::Class::Storage::DBI::mysql >;
  3         9  
  3         3631  
8              
9 3     3   525628 use Algorithm::Backoff::RetryTimeouts;
  3         26880  
  3         128  
10 3     3   24 use Context::Preserve;
  3         6  
  3         218  
11 3     3   2798 use DBIx::ParseError::MySQL;
  3         475420  
  3         142  
12 3     3   32 use List::Util qw< min max >;
  3         11  
  3         331  
13 3     3   20 use Scalar::Util qw< blessed >;
  3         7  
  3         195  
14 3     3   22 use Storable qw< dclone >;
  3         6  
  3         201  
15 3     3   21 use Time::HiRes qw< time sleep >;
  3         7  
  3         32  
16 3     3   261 use namespace::clean;
  3         6  
  3         34  
17              
18             # ABSTRACT: MySQL-specific DBIC storage engine with retry support
19 3     3   1067 use version;
  3         8  
  3         25  
20             our $VERSION = 'v1.0.3'; # VERSION
21              
22             #pod =head1 SYNOPSIS
23             #pod
24             #pod package MySchema;
25             #pod
26             #pod # Recommended
27             #pod DBIx::Class::Storage::DBI::mysql::Retryable->_use_join_optimizer(0);
28             #pod
29             #pod __PACKAGE__->storage_type('::DBI::mysql::Retryable');
30             #pod
31             #pod # Optional settings (defaults shown)
32             #pod my $storage_class = 'DBIx::Class::Storage::DBI::mysql::Retryable';
33             #pod $storage_class->parse_error_class('DBIx::ParseError::MySQL');
34             #pod $storage_class->timer_class('Algorithm::Backoff::RetryTimeouts');
35             #pod $storage_class->timer_options({}); # same defaults as the timer class
36             #pod $storage_class->aggressive_timeouts(0);
37             #pod $storage_class->retries_before_error_prefix(1);
38             #pod $storage_class->warn_on_retryable_error(0);
39             #pod $storage_class->enable_retryable(1);
40             #pod
41             #pod =head1 DESCRIPTION
42             #pod
43             #pod This storage engine for L is a MySQL-specific engine that will explicitly
44             #pod retry on MySQL-specific transient error messages, as identified by L,
45             #pod using L as its retry algorithm. This engine should be
46             #pod much better at handling deadlocks, connection errors, and Galera node flips to ensure the
47             #pod transaction always goes through.
48             #pod
49             #pod =head2 How Retryable Works
50             #pod
51             #pod A DBIC command triggers some sort of connection to the MySQL server to send SQL. First,
52             #pod Retryable makes sure the connection C values (except C
53             #pod unless L is set) are set properly. (The default settings for
54             #pod L will use half of the
55             #pod maximum duration, with some jitter.) If the connection was successful, a few C
56             #pod commands for timeouts are sent first:
57             #pod
58             #pod wait_timeout # only with aggressive_timeouts=1
59             #pod lock_wait_timeout
60             #pod innodb_lock_wait_timeout
61             #pod net_read_timeout
62             #pod net_write_timeout
63             #pod
64             #pod If the DBIC command fails at any point in the process, and the error is a recoverable
65             #pod failure (according to the L), the retry
66             #pod process starts.
67             #pod
68             #pod The timeouts are only checked during the retry handler. Since DB operations are XS
69             #pod calls, Perl-style "safe" ALRM signals won't do any good, and the engine won't attempt to
70             #pod use unsafe ones. Thus, the engine relies on the server to honor the timeouts set during
71             #pod each attempt, and will give up if it runs out of time or attempts.
72             #pod
73             #pod If the DBIC command succeeds during the process, program flow resumes as normal. If any
74             #pod re-attempts happened during the DBIC command, the timeouts are reset back to the original
75             #pod post-connection values.
76             #pod
77             #pod =head1 STORAGE OPTIONS
78             #pod
79             #pod =cut
80              
81             __PACKAGE__->mk_group_accessors('inherited' => qw<
82             parse_error_class timer_class
83             timer_options aggressive_timeouts
84             retries_before_error_prefix
85             warn_on_retryable_error enable_retryable
86             >);
87              
88             __PACKAGE__->mk_group_accessors('simple' => qw<
89             _retryable_timer _retryable_current_timeout
90             _retryable_call_type _retryable_exception_prefix
91             _retryable_original_die_handler
92             >);
93              
94             # Set defaults
95             __PACKAGE__->parse_error_class('DBIx::ParseError::MySQL');
96             __PACKAGE__->timer_class('Algorithm::Backoff::RetryTimeouts');
97             __PACKAGE__->timer_options({});
98             __PACKAGE__->aggressive_timeouts(0);
99             __PACKAGE__->retries_before_error_prefix(1);
100             __PACKAGE__->warn_on_retryable_error(0);
101             __PACKAGE__->enable_retryable(1);
102              
103             #pod =head2 parse_error_class
104             #pod
105             #pod Class used to parse MySQL error messages.
106             #pod
107             #pod Default is L. If a different class is used, it must support a
108             #pod similar interface, especially the L|DBIx::ParseError::MySQL/is_transient>
109             #pod method.
110             #pod
111             #pod =head2 timer_class
112             #pod
113             #pod Algorithm class used to determine timeout and sleep values during the retry process.
114             #pod
115             #pod Default is L. If a different class is used, it must
116             #pod support a similar interface, including the dual return of the L|Algorithm::Backoff::RetryTimeouts/failure>
117             #pod method.
118             #pod
119             #pod =head2 timer_options
120             #pod
121             #pod Options to pass to the timer algorithm constructor, as a hashref.
122             #pod
123             #pod Default is an empty hashref, which would retain all of the defaults of the algorithm
124             #pod module.
125             #pod
126             #pod =head2 aggressive_timeouts
127             #pod
128             #pod Boolean that controls whether to use some of the more aggressive, query-unfriendly
129             #pod timeouts:
130             #pod
131             #pod =over
132             #pod
133             #pod =item mysql_read_timeout
134             #pod
135             #pod Controls the timeout for all read operations. Since SQL queries in the middle of
136             #pod sending its first set of row data are still considered to be in a read operation, those
137             #pod queries could time out during those circumstances.
138             #pod
139             #pod If you're confident that you don't have any SQL statements that would take longer than
140             #pod C (or at least returning results before that time), you can turn this option on.
141             #pod Otherwise, you may experience longer-running statements going into a retry death spiral
142             #pod until they finally hit the Retryable timeout for good and die.
143             #pod
144             #pod =item wait_timeout
145             #pod
146             #pod Controls how long the MySQL server waits for activity from the connection before timing
147             #pod out. While most applications are going to be using the database connection pretty
148             #pod frequently, the MySQL default (8 hours) is much much longer than the mere seconds this
149             #pod engine would set it to.
150             #pod
151             #pod =back
152             #pod
153             #pod Default is off. Obviously, this setting only makes sense with L
154             #pod turned on.
155             #pod
156             #pod =head2 retries_before_error_prefix
157             #pod
158             #pod Controls the number of retries (not tries) needed before the exception message starts
159             #pod using the statistics prefix, which looks something like this:
160             #pod
161             #pod Failed dbh_do coderef: Out of retries, attempts: 5 / 4, timer: 34.5 / 50.0 sec
162             #pod
163             #pod The default is 1, which means a failed first attempt (like a non-transient failure) will
164             #pod show a normal exception, and the second attempt will use the prefix. You can set this to
165             #pod 0 to always show the prefix, or a large number like 99 to keep the exception clean.
166             #pod
167             #pod =head2 warn_on_retryable_error
168             #pod
169             #pod Boolean that controls whether to warn on retryable failures, as the engine encounters
170             #pod them. Many applications don't want spam on their screen for recoverable conditions, but
171             #pod this may be useful for debugging or CLI tools.
172             #pod
173             #pod Unretryable failures always generate an exception as normal, regardless of the setting.
174             #pod
175             #pod This is functionally equivalent to L, but since L<"RaiseError"|DBI/RaiseError>
176             #pod is already the DBIC-required default, the former option can't be used within DBI.
177             #pod
178             #pod Default is off.
179             #pod
180             #pod =head2 enable_retryable
181             #pod
182             #pod Boolean that enables the Retryable logic. This can be turned off to temporarily disable
183             #pod it, and revert to DBIC's basic "retry once if disconnected" default. This may be useful
184             #pod if a process is already using some other retry logic (like L).
185             #pod
186             #pod Messing with this setting in the middle of a database action would not be wise.
187             #pod
188             #pod Default is on.
189             #pod
190             #pod =for Pod::Coverage max_attempts retryable_timeout disable_retryable
191             #pod
192             #pod =cut
193              
194             ### Backward-compatibility for legacy attributes
195              
196             sub max_attempts {
197 5     5 0 42191 my $self = shift;
198 5         274 my $opts = $self->timer_options;
199              
200 5 100       675 return $opts->{max_attempts} = $_[0] if @_;
201 3   100     86 return $opts->{max_attempts} // 8;
202             }
203              
204             sub retryable_timeout {
205 22     22 0 198513 my $self = shift;
206 22         1017 my $opts = $self->timer_options;
207              
208 22 100       1130 return $opts->{max_actual_duration} = $_[0] if @_;
209 20   100     126 return $opts->{max_actual_duration} // 50;
210             }
211              
212             sub disable_retryable {
213 4     4 0 558693 my $self = shift;
214 4 100       215 $self->enable_retryable( $_[0] ? 0 : 1 ) if @_;
    50          
215 4 100       261 return $self->enable_retryable ? 0 : 1;
216             }
217              
218             #pod =head1 METHODS
219             #pod
220             #pod =cut
221              
222             sub _build_retryable_timer {
223 53     53   144 my $self = shift;
224             return $self->timer_class->new(
225 53         2184 %{ dclone $self->timer_options }
  53         3598  
226             );
227             }
228              
229             sub _reset_retryable_timeout {
230 28     28   11831 my $self = shift;
231              
232             # Use a temporary timer to get the first timeout value
233 28         105 my $timeout = $self->_build_retryable_timer->timeout;
234 28 100       6894 $timeout = 0 if $timeout == -1;
235 28         143 $self->_retryable_current_timeout($timeout);
236             }
237              
238 31   50 31   433 sub _failed_attempt_count { shift->_retryable_timer->{_attempts} // 0 }
239              
240             # Constructor
241             sub new {
242 2     2 1 1274323 my $self = shift->next::method(@_);
243              
244 2         1978 $self->_reset_retryable_timeout;
245              
246 2         572 $self;
247             }
248              
249             # Return the list of timeout strings to check
250             sub _timeout_set_list {
251 8     8   69 my ($self, $type) = @_;
252              
253 8         19 my @timeout_set;
254 8 100       55 if ($type eq 'dbi') {
    50          
255 3         12 @timeout_set = (qw< connect write >);
256 3 50       103 push @timeout_set, 'read' if $self->aggressive_timeouts;
257              
258 3         104 @timeout_set = map { "mysql_${_}_timeout" } @timeout_set;
  6         26  
259             }
260             elsif ($type eq 'session') {
261 5         22 @timeout_set = (qw< lock_wait innodb_lock_wait net_read net_write >);
262 5 50       174 push @timeout_set, 'wait' if $self->aggressive_timeouts;
263              
264 5         160 @timeout_set = map { "${_}_timeout" } @timeout_set;
  20         60  
265             }
266             else {
267 0         0 die "Unknown mysql timeout set: $type";
268             }
269              
270 8         80 return @timeout_set;
271             }
272              
273             # Set the timeouts for reconnections by inserting them into the default DBI connection
274             # attributes.
275             sub _default_dbi_connect_attributes () {
276 14     14   1768 my $self = shift;
277 14 100 100     345 return $self->next::method unless $self->_retryable_current_timeout && $self->enable_retryable;
278              
279 3         131 my $timeout = int( $self->_retryable_current_timeout + 0.5 );
280              
281             return +{
282 6         17 (map {; $_ => $timeout } $self->_timeout_set_list('dbi')), # set timeouts
283             mysql_auto_reconnect => 0, # do not use MySQL's own reconnector
284 3         16 %{ $self->next::method }, # inherit the other default attributes
  3         21  
285             };
286             }
287              
288             # Re-apply the timeout settings above on _dbi_connect_info. Used after the initial
289             # connection by the retry handling.
290             sub _set_dbi_connect_info {
291 0     0   0 my $self = shift;
292 0 0 0     0 return unless $self->_retryable_current_timeout && $self->enable_retryable;
293              
294 0         0 my $timeout = int( $self->_retryable_current_timeout + 0.5 );
295              
296 0         0 my $info = $self->_dbi_connect_info;
297              
298             # Not even going to attempt this one...
299 0 0       0 if (ref $info eq 'CODE') {
300 0 0       0 warn <<"EOW" unless $ENV{DBIC_RETRYABLE_DONT_SET_CONNECT_SESSION_VARS};
301              
302             ***************************************************************************
303             Your connect_info is a coderef, which means connection-based MySQL timeouts
304             cannot be dynamically changed. Under certain conditions, the connection (or
305             combination of connection attempts) may take longer to timeout than your
306             current timer settings.
307              
308             You'll want to revert to a 4-element style DBI argument set, to fully
309             support the timeout functionality.
310              
311             To disable this warning, set a true value to the environment variable
312             DBIC_RETRYABLE_DONT_SET_CONNECT_SESSION_VARS
313              
314             ***************************************************************************
315             EOW
316 0         0 return;
317             }
318              
319 0         0 my $dbi_attr = $info->[3];
320 0 0 0     0 return unless $dbi_attr && ref $dbi_attr eq 'HASH';
321              
322 0         0 $dbi_attr->{$_} = $timeout for $self->_timeout_set_list('dbi');
323             }
324              
325             # Set session timeouts for post-connection variables
326             sub _run_connection_actions {
327 23     23   9557 my $self = shift;
328 23         172 $self->_set_retryable_session_timeouts;
329 23         160 $self->next::method(@_);
330             }
331              
332             sub _set_retryable_session_timeouts {
333 23     23   95 my $self = shift;
334 23 100 100     363 return unless $self->_retryable_current_timeout && $self->enable_retryable;
335              
336 5         199 my $timeout = int( $self->_retryable_current_timeout + 0.5 );
337              
338             # Ironically, we aren't running our own SET SESSION commands with their own
339             # BlockRunner protection, since that may lead to infinite stack recursion. Instead,
340             # put it in a basic eval, and do a quick is_transient check. If it passes, let the
341             # next *_do/_do_query call handle it.
342              
343 5         12 local $@;
344 5         13 eval {
345             # Again, don't want to let outside handlers ruin our error checking. This
346             # expires before our 'die' statements below.
347 5         27 local $SIG{__DIE__};
348              
349 5         18 my $dbh = $self->_dbh;
350 5 50       17 if ($dbh) {
351 5         63 $dbh->do("SET SESSION $_=$timeout") for $self->_timeout_set_list('session');
352             }
353             };
354             # Protect $@ again, just in case the parser class does something inappropriate
355             # with a blessed $error
356 5 50       197 if ( my $error = $@ ) {
357 0 0       0 die unless do { # bare die for $@ propagation
358 0         0 local $@;
359 0         0 $self->parse_error_class->new($error)->is_transient;
360             };
361              
362             # The error may have been transient, but we might have ran out of retries, anyway
363 0 0       0 die if $error =~ m;
364              
365 0 0       0 warn "Encountered a recoverable error during SET SESSION timeout commands: $error" if $self->warn_on_retryable_error;
366             }
367             }
368              
369             # Make sure the initial connection call is protected from retryable failures
370             sub _connect {
371 21     21   30739 my $self = shift;
372 21 100       763 return $self->next::method() unless $self->enable_retryable;
373             # next::can here to do mro calculations prior to sending to _blockrunner_do
374 19         657 return $self->_blockrunner_do( connect => $self->next::can() );
375             }
376              
377             #pod =head2 dbh_do
378             #pod
379             #pod my $val = $schema->storage->dbh_do(
380             #pod sub {
381             #pod my ($storage, $dbh, @binds) = @_;
382             #pod $dbh->selectrow_array($sql, undef, @binds);
383             #pod },
384             #pod @passed_binds,
385             #pod );
386             #pod
387             #pod This is very much like L, except it doesn't require a
388             #pod connection failure to retry the sub block. Instead, it will also retry on locks, query
389             #pod interruptions, and failovers.
390             #pod
391             #pod Normal users of DBIC typically won't use this method directly. Instead, any ResultSet
392             #pod or Result method that contacts the DB will send its SQL through here, and protect it from
393             #pod retryable failures.
394             #pod
395             #pod However, this method is recommended over using C<< $schema->storage->dbh >> directly to
396             #pod run raw SQL statements.
397             #pod
398             #pod =cut
399              
400             # Main "doer" method for both dbh_do and txn_do
401             sub _blockrunner_do {
402 41     41   388 my $self = shift;
403 41         99 my $call_type = shift;
404 41         92 my $run_target = shift;
405              
406             # See https://metacpan.org/release/DBIx-Class/source/lib/DBIx/Class/Storage/DBI.pm#L842
407 41 100       184 my $args = @_ ? \@_ : [];
408              
409             my $target_runner = sub {
410             # dbh_do and txn_do have different sub arguments, and _connect shouldn't
411             # have a _get_dbh call.
412 59 100   59   13497 if ($call_type eq 'txn_do') { $run_target->( @$args ); }
  11 100       63  
    50          
413 28         179 elsif ($call_type eq 'dbh_do') { $self->$run_target( $self->_get_dbh, @$args ); }
414 20         114 elsif ($call_type eq 'connect') { $self->$run_target( @$args ); }
415 0         0 else { die "Unknown call type: $call_type" }
416 41         252 };
417              
418             # Transaction depth short circuit (same as DBIx::Class::Storage::DBI)
419 41 100 66     395 return $target_runner->() if $self->{_in_do_block} || $self->transaction_depth;
420              
421             # Given our transaction depth short circuits, we should be at the outermost loop,
422             # so it's safe to reset our variables.
423 25         543 $self->_retryable_timer( $self->_build_retryable_timer );
424              
425 25         5121 my $timeout = $self->_retryable_timer->timeout;
426 25 100       558 $timeout = 0 if $timeout == -1;
427 25         106 $self->_retryable_current_timeout($timeout);
428 25         107 $self->_retryable_call_type($call_type);
429              
430             # We have some post-processing to do, so save the BlockRunner object, and then save
431             # the result in a context-sensitive manner.
432 25         1276 my $br = DBIx::Class::Storage::BlockRunner->new(
433             storage => $self,
434             wrap_txn => $call_type eq 'txn_do',
435              
436             # This neuters the max_attempts trigger in failed_attempt_count, so that the main check
437             # in our retry_handler works as expected.
438             max_attempts => 99999,
439              
440             retry_handler => \&_blockrunner_retry_handler,
441             );
442              
443             ### XXX: Outside exception handlers shouldn't interrupt the retry process, as it might never
444             ### never return back from the eval. This should really be a part of BlockRunner, but it's
445             ### not our module, so we hit the "local $SIG{__DIE__}" bit here. What that means is that
446             ### we're removing the die handler a bit too high up in the process, and we have exception
447             ### throwing that should use the outside handler.
448             ###
449             ### Also, BlockRunner doesn't always protect its $@ result from DBI evals, so we also need to
450             ### protect that here.
451             ###
452             ### So, we save it here, and throw it out when we're done.
453              
454 25         6786 $self->_retryable_original_die_handler( $SIG{__DIE__} );
455              
456             return preserve_context {
457 25     25   512 local $@;
458 25         110 local $SIG{__DIE__};
459 25         136 $br->run($target_runner);
460             }
461 25     22   759 after => sub { $self->_reset_timers_and_timeouts };
  22         11575931  
462              
463 0         0 $self->_retryable_original_die_handler(undef);
464             }
465              
466             # Our own BlockRunner retry handler
467             sub _blockrunner_retry_handler {
468 21     21   29137 my $br = shift;
469 21         83 my $self = $br->storage; # "self" for this module
470              
471 21         97 my $last_error = $br->last_exception;
472              
473             # Record the failure in the timer algorithm (prior to any checks)
474 21         1119 my ($sleep_time, $new_timeout) = $self->_retryable_timer->failure;
475              
476             # If it's not a retryable error, stop here
477 21         5924 my $parsed_error = $self->parse_error_class->new($last_error);
478 21 100       10687 return $self->_reset_and_fail('Exception not transient') unless $parsed_error->is_transient;
479              
480 19         8133 $last_error =~ s/\n.+//s;
481 19 50       872 $self->_warn_retryable_error($last_error) if $self->warn_on_retryable_error;
482              
483             # Either stop here (because of timeout or max attempts), sleep, or don't
484 19 100       770 if ($sleep_time == -1) { return $self->_reset_and_fail('Out of retries') }
  1 100       9  
485 10         15006987 elsif ($sleep_time) { sleep $sleep_time; }
486              
487 18 50       241 if ($new_timeout > 0) {
488             # Reset the connection timeouts before we connect again
489 0         0 $self->_retryable_current_timeout($new_timeout);
490 0         0 $self->_set_dbi_connect_info;
491             }
492              
493             # Force a disconnect, but only if the connection seems to be in a broken state
494 18         56 local $@;
495 18 100       1833 unless ($parsed_error->error_type eq 'lock') {
496 5         97 eval { local $SIG{__DIE__}; $self->disconnect };
  5         59  
  5         69  
497             }
498              
499             # Because BlockRunner calls this unprotected, and because our own _connect is going
500             # to hit the _in_do_block short-circuit, we should call this ourselves, in a
501             # protected eval, and re-direct any errors as if it was another failed attempt.
502 18         1620 eval { $self->ensure_connected };
  18         208  
503 18 50       3493 if (my $connect_error = $@) {
504 0         0 push @{ $br->exception_stack }, $connect_error;
  0         0  
505 0         0 return _blockrunner_retry_handler($br);
506             }
507              
508 18         178 return 1;
509             }
510              
511             sub dbh_do {
512 19     19 1 90830 my $self = shift;
513 19 100       808 return $self->next::method(@_) unless $self->enable_retryable;
514 17         390 return $self->_blockrunner_do( dbh_do => @_ );
515             }
516              
517             #pod =head2 txn_do
518             #pod
519             #pod my $val = $schema->txn_do(
520             #pod sub {
521             #pod # ...DBIC calls within transaction...
522             #pod },
523             #pod @misc_args_passed_to_coderef,
524             #pod );
525             #pod
526             #pod Works just like L, except it's now protected against
527             #pod retryable failures.
528             #pod
529             #pod Calling this method through the C<$schema> object is typically more convenient.
530             #pod
531             #pod =cut
532              
533             sub txn_do {
534 5     5 1 8238 my $self = shift;
535 5 50       139 return $self->next::method(@_) unless $self->enable_retryable;
536              
537             # Connects or reconnects on pid change to grab correct txn_depth (same as
538             # DBIx::Class::Storage::DBI)
539 5         116 $self->_get_dbh;
540              
541 5         1493 $self->_blockrunner_do( txn_do => @_ );
542             }
543              
544             #pod =for Pod::Coverage is_dbi_error_retryable
545             #pod
546             #pod =cut
547              
548             ### XXX: This is a now deprecated method that only existed in the non-public version, but
549             ### it's a public method that should still exist for anybody previously using it.
550             sub is_dbi_error_retryable {
551 2     2 0 12404 my ($self, $error) = @_;
552 2         97 return $self->parse_error_class->new($error)->is_transient;
553             }
554              
555             #pod =head2 throw_exception
556             #pod
557             #pod $storage->throw_exception('It failed');
558             #pod
559             #pod Works just like L, but also reports attempt and
560             #pod timer statistics, in case the transaction was tried multiple times.
561             #pod
562             #pod =cut
563              
564             sub _reset_timers_and_timeouts {
565 25     25   84 my $self = shift;
566              
567             # Only reset timeouts if we have to, but check before we clear
568 25   66     117 my $needs_resetting = $self->_failed_attempt_count && $self->_retryable_current_timeout;
569              
570 25         187 $self->_retryable_timer(undef);
571 25         107 $self->_reset_retryable_timeout;
572              
573 25 50       142 if ($needs_resetting) {
574 0         0 $self->_set_dbi_connect_info;
575 0         0 $self->_set_retryable_session_timeouts;
576             }
577              
578             # Useful for chaining to the return call in _blockrunner_retry_handler
579 25         140 return undef;
580             }
581              
582             sub _warn_retryable_error {
583 0     0   0 my ($self, $error) = @_;
584              
585 0         0 my $timer = $self->_retryable_timer;
586 0         0 my $current_attempt_count = $self->_failed_attempt_count + 1;
587             my $debug_msg = sprintf(
588             'Retrying %s coderef, attempt %u of %u, timer: %.1f / %.1f sec, last exception: %s',
589             $self->_retryable_call_type,
590             $current_attempt_count, $self->max_attempts,
591             $timer->{_last_timestamp} - $timer->{_start_timestamp}, $timer->{max_actual_duration},
592 0         0 $error
593             );
594              
595 0         0 warn $debug_msg;
596             }
597              
598             sub _reset_and_fail {
599 3     3   634 my ($self, $fail_reason) = @_;
600              
601             # About to throw the main exception, so set the original handler
602 3         25 $SIG{__DIE__} = $self->_retryable_original_die_handler;
603              
604             # First error (by default): just pass the exception unaltered
605 3 50       17 if ($self->_failed_attempt_count <= $self->retries_before_error_prefix) {
606 0         0 $self->_retryable_exception_prefix(undef);
607 0         0 return $self->_reset_timers_and_timeouts;
608             }
609              
610 3         71 my $timer = $self->_retryable_timer;
611             $self->_retryable_exception_prefix( sprintf(
612             'Failed %s coderef: %s, attempts: %u / %u, timer: %.1f / %.1f sec',
613             $self->_retryable_call_type, $fail_reason,
614             $self->_failed_attempt_count, $self->max_attempts,
615             $timer->{_last_timestamp} - $timer->{_start_timestamp}, $timer->{max_actual_duration},
616 3         22 ) );
617              
618 3         17 return $self->_reset_timers_and_timeouts;
619             }
620              
621             sub throw_exception {
622 26     26 1 42037766 my $self = shift;
623              
624             # Clear the prefix as we use it
625 26         163 my $exception_prefix = $self->_retryable_exception_prefix;
626 26 100       488 $self->_retryable_exception_prefix(undef) if $exception_prefix;
627              
628 26 100       523 return $self->next::method(@_) unless $exception_prefix;
629              
630 3         30 my $error = shift;
631 3         10 $exception_prefix .= ', last exception: ';
632 3 50 33     42 if (blessed $error && $error->isa('DBIx::Class::Exception')) {
633 3         19 $error->{msg} = $exception_prefix.$error->{msg};
634             }
635             else {
636 0         0 $error = $exception_prefix.$error;
637             }
638 3         21 return $self->next::method($error, @_);
639             }
640              
641             #pod =head1 CAVEATS
642             #pod
643             #pod =head2 Transactions without txn_do
644             #pod
645             #pod Retryable is transaction-safe. Only the outermost transaction depth gets the retry
646             #pod protection, since that's the only layer that is idempotent and atomic.
647             #pod
648             #pod However, transaction commands like C and C are NOT granted
649             #pod retry protection, because DBIC/Retryable does not have a defined transaction-safe code
650             #pod closure to use upon reconnection. Only C will have the protections available.
651             #pod
652             #pod For example:
653             #pod
654             #pod # Has retry protetion
655             #pod my $rs = $schema->resultset('Foo');
656             #pod $rs->delete;
657             #pod
658             #pod # This effectively turns off retry protection
659             #pod $schema->txn_begin;
660             #pod
661             #pod # NOT protected from retryable errors!
662             #pod my $result = $rs->create({bar => 12});
663             #pod $result->update({baz => 42});
664             #pod
665             #pod $schema->txn_commit;
666             #pod # Retry protection is back on
667             #pod
668             #pod # Do this instead!
669             #pod $schema->txn_do(sub {
670             #pod my $result = $rs->create({bar => 12});
671             #pod $result->update({baz => 42});
672             #pod });
673             #pod
674             #pod # Still has retry protection
675             #pod $rs->delete;
676             #pod
677             #pod All of this behavior mimics how DBIC's original storage engines work.
678             #pod
679             #pod =head2 (Ab)using $dbh directly
680             #pod
681             #pod Similar to C, directly accessing and using a DBI database or statement handle
682             #pod does NOT grant retry protection, even if they are acquired from the storage engine via
683             #pod C<< $storage->dbh >>.
684             #pod
685             #pod Instead, use L. This method is also used by DBIC for most of its active DB
686             #pod calls, after it has composed a proper SQL statement to run.
687             #pod
688             #pod =head1 SEE ALSO
689             #pod
690             #pod L - A similar engine for DBI connections, using L as a base.
691             #pod
692             #pod L - Base module in DBIC that controls how transactional coderefs are ran and retried
693             #pod
694             #pod =cut
695              
696             1;
697              
698             __END__