File Coverage

blib/lib/DBIx/Class/Storage/DBI/mysql/Retryable.pm
Criterion Covered Total %
statement 167 200 83.5
branch 56 84 66.6
condition 16 27 59.2
subroutine 36 38 94.7
pod 4 8 50.0
total 279 357 78.1


line stmt bran cond sub pod time code
1             package DBIx::Class::Storage::DBI::mysql::Retryable;
2              
3 2     2   339450 use strict;
  2         8  
  2         48  
4 2     2   8 use warnings;
  2         4  
  2         43  
5              
6 2     2   2636 use DBI '1.630';
  2         29857  
  2         318  
7 2     2   13 use base qw< DBIx::Class::Storage::DBI::mysql >;
  2         4  
  2         862  
8              
9 2     2   216930 use Algorithm::Backoff::RetryTimeouts;
  2         11606  
  2         59  
10 2     2   13 use Context::Preserve;
  2         4  
  2         88  
11 2     2   716 use DBIx::ParseError::MySQL;
  2         157556  
  2         65  
12 2     2   15 use List::Util qw< min max >;
  2         5  
  2         133  
13 2     2   11 use Scalar::Util qw< blessed >;
  2         4  
  2         70  
14 2     2   10 use Storable qw< dclone >;
  2         3  
  2         88  
15 2     2   12 use Time::HiRes qw< time sleep >;
  2         3  
  2         16  
16 2     2   221 use namespace::clean;
  2         5  
  2         16  
17              
18             # ABSTRACT: MySQL-specific DBIC storage engine with retry support
19 2     2   563 use version;
  2         5  
  2         12  
20             our $VERSION = 'v1.0.2'; # 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 =cut
191              
192             ### Backward-compatibility for legacy attributes
193              
194             sub max_attempts {
195 4     4 0 23190 my $self = shift;
196 4         146 my $opts = $self->timer_options;
197              
198 4 100       171 return $opts->{max_attempts} = $_[0] if @_;
199 2   100     90 return $opts->{max_attempts} // 8;
200             }
201              
202             sub retryable_timeout {
203 18     18 0 90815 my $self = shift;
204 18         480 my $opts = $self->timer_options;
205              
206 18 100       672 return $opts->{max_actual_duration} = $_[0] if @_;
207 16   100     81 return $opts->{max_actual_duration} // 50;
208             }
209              
210             sub disable_retryable {
211 4     4 0 524801 my $self = shift;
212 4 100       197 $self->enable_retryable( $_[0] ? 0 : 1 ) if @_;
    50          
213 4 100       191 return $self->enable_retryable ? 0 : 1;
214             }
215              
216             #pod =head1 METHODS
217             #pod
218             #pod =cut
219              
220             sub _build_retryable_timer {
221 37     37   72 my $self = shift;
222             return $self->timer_class->new(
223 37         985 %{ dclone $self->timer_options }
  37         1828  
224             );
225             }
226              
227             sub _reset_retryable_timeout {
228 20     20   6717 my $self = shift;
229              
230             # Use a temporary timer to get the first timeout value
231 20         128 my $timeout = $self->_build_retryable_timer->timeout;
232 20 100       4064 $timeout = 0 if $timeout == -1;
233 20         88 $self->_retryable_current_timeout($timeout);
234             }
235              
236 21   50 21   235 sub _failed_attempt_count { shift->_retryable_timer->{_attempts} // 0 }
237              
238             # Constructor
239             sub new {
240 2     2 1 31698 my $self = shift->next::method(@_);
241              
242 2         1327 $self->_reset_retryable_timeout;
243              
244 2         358 $self;
245             }
246              
247             # Return the list of timeout strings to check
248             sub _timeout_set_list {
249 8     8   20 my ($self, $type) = @_;
250              
251 8         12 my @timeout_set;
252 8 100       27 if ($type eq 'dbi') {
    50          
253 3         21 @timeout_set = (qw< connect write >);
254 3 50       67 push @timeout_set, 'read' if $self->aggressive_timeouts;
255              
256 3         88 @timeout_set = map { "mysql_${_}_timeout" } @timeout_set;
  6         24  
257             }
258             elsif ($type eq 'session') {
259 5         15 @timeout_set = (qw< lock_wait innodb_lock_wait net_read net_write >);
260 5 50       88 push @timeout_set, 'wait' if $self->aggressive_timeouts;
261              
262 5         103 @timeout_set = map { "${_}_timeout" } @timeout_set;
  20         47  
263             }
264             else {
265 0         0 die "Unknown mysql timeout set: $type";
266             }
267              
268 8         41 return @timeout_set;
269             }
270              
271             # Set the timeouts for reconnections by inserting them into the default DBI connection
272             # attributes.
273             sub _default_dbi_connect_attributes () {
274 10     10   1083 my $self = shift;
275 10 100 100     172 return $self->next::method unless $self->_retryable_current_timeout && $self->enable_retryable;
276              
277 3         95 my $timeout = int( $self->_retryable_current_timeout + 0.5 );
278              
279             return +{
280 6         14 (map {; $_ => $timeout } $self->_timeout_set_list('dbi')), # set timeouts
281             mysql_auto_reconnect => 0, # do not use MySQL's own reconnector
282 3         12 %{ $self->next::method }, # inherit the other default attributes
  3         14  
283             };
284             }
285              
286             # Re-apply the timeout settings above on _dbi_connect_info. Used after the initial
287             # connection by the retry handling.
288             sub _set_dbi_connect_info {
289 0     0   0 my $self = shift;
290 0 0 0     0 return unless $self->_retryable_current_timeout && $self->enable_retryable;
291              
292 0         0 my $timeout = int( $self->_retryable_current_timeout + 0.5 );
293              
294 0         0 my $info = $self->_dbi_connect_info;
295              
296             # Not even going to attempt this one...
297 0 0       0 if (ref $info eq 'CODE') {
298 0 0       0 warn <<"EOW" unless $ENV{DBIC_RETRYABLE_DONT_SET_CONNECT_SESSION_VARS};
299              
300             ***************************************************************************
301             Your connect_info is a coderef, which means connection-based MySQL timeouts
302             cannot be dynamically changed. Under certain conditions, the connection (or
303             combination of connection attempts) may take longer to timeout than your
304             current timer settings.
305              
306             You'll want to revert to a 4-element style DBI argument set, to fully
307             support the timeout functionality.
308              
309             To disable this warning, set a true value to the environment variable
310             DBIC_RETRYABLE_DONT_SET_CONNECT_SESSION_VARS
311              
312             ***************************************************************************
313             EOW
314 0         0 return;
315             }
316              
317 0         0 my $dbi_attr = $info->[3];
318 0 0 0     0 return unless $dbi_attr && ref $dbi_attr eq 'HASH';
319              
320 0         0 $dbi_attr->{$_} = $timeout for $self->_timeout_set_list('dbi');
321             }
322              
323             # Set session timeouts for post-connection variables
324             sub _run_connection_actions {
325 17     17   5975 my $self = shift;
326 17         64 $self->_set_retryable_session_timeouts;
327 17         89 $self->next::method(@_);
328             }
329              
330             sub _set_retryable_session_timeouts {
331 17     17   32 my $self = shift;
332 17 100 100     232 return unless $self->_retryable_current_timeout && $self->enable_retryable;
333              
334 5         132 my $timeout = int( $self->_retryable_current_timeout + 0.5 );
335              
336             # Ironically, we aren't running our own SET SESSION commands with their own
337             # BlockRunner protection, since that may lead to infinite stack recursion. Instead,
338             # put it in a basic eval, and do a quick is_transient check. If it passes, let the
339             # next *_do/_do_query call handle it.
340              
341 5         12 local $@;
342 5         8 eval {
343             # Again, don't want to let outside handlers ruin our error checking. This
344             # expires before our 'die' statements below.
345 5         20 local $SIG{__DIE__};
346              
347 5         12 my $dbh = $self->_dbh;
348 5 50       13 if ($dbh) {
349 5         12 $dbh->do("SET SESSION $_=$timeout") for $self->_timeout_set_list('session');
350             }
351             };
352             # Protect $@ again, just in case the parser class does something inappropriate
353             # with a blessed $error
354 5 50       152 if ( my $error = $@ ) {
355 0 0       0 die unless do { # bare die for $@ propagation
356 0         0 local $@;
357 0         0 $self->parse_error_class->new($error)->is_transient;
358             };
359              
360             # The error may have been transient, but we might have ran out of retries, anyway
361 0 0       0 die if $error =~ m;
362              
363 0 0       0 warn "Encountered a recoverable error during SET SESSION timeout commands: $error" if $self->warn_on_retryable_error;
364             }
365             }
366              
367             # Make sure the initial connection call is protected from retryable failures
368             sub _connect {
369 15     15   19266 my $self = shift;
370 15 100       326 return $self->next::method() unless $self->enable_retryable;
371             # next::can here to do mro calculations prior to sending to _blockrunner_do
372 13         325 return $self->_blockrunner_do( connect => $self->next::can() );
373             }
374              
375             #pod =head2 dbh_do
376             #pod
377             #pod my $val = $schema->storage->dbh_do(
378             #pod sub {
379             #pod my ($storage, $dbh, @binds) = @_;
380             #pod $dbh->selectrow_array($sql, undef, @binds);
381             #pod },
382             #pod @passed_binds,
383             #pod );
384             #pod
385             #pod This is very much like L, except it doesn't require a
386             #pod connection failure to retry the sub block. Instead, it will also retry on locks, query
387             #pod interruptions, and failovers.
388             #pod
389             #pod Normal users of DBIC typically won't use this method directly. Instead, any ResultSet
390             #pod or Result method that contacts the DB will send its SQL through here, and protect it from
391             #pod retryable failures.
392             #pod
393             #pod However, this method is recommended over using C<< $schema->storage->dbh >> directly to
394             #pod run raw SQL statements.
395             #pod
396             #pod =cut
397              
398             # Main "doer" method for both dbh_do and txn_do
399             sub _blockrunner_do {
400 21     21   194 my $self = shift;
401 21         42 my $call_type = shift;
402 21         43 my $run_target = shift;
403              
404             # See https://metacpan.org/release/DBIx-Class/source/lib/DBIx/Class/Storage/DBI.pm#L842
405 21 100       92 my $args = @_ ? \@_ : [];
406              
407             my $target_runner = sub {
408             # dbh_do and txn_do have different sub arguments, and _connect shouldn't
409             # have a _get_dbh call.
410 33 100   33   5716 if ($call_type eq 'txn_do') { $run_target->( @$args ); }
  1 100       4  
    50          
411 18         92 elsif ($call_type eq 'dbh_do') { $self->$run_target( $self->_get_dbh, @$args ); }
412 14         75 elsif ($call_type eq 'connect') { $self->$run_target( @$args ); }
413 0         0 else { die "Unknown call type: $call_type" }
414 21         126 };
415              
416             # Transaction depth short circuit (same as DBIx::Class::Storage::DBI)
417 21 100 66     191 return $target_runner->() if $self->{_in_do_block} || $self->transaction_depth;
418              
419             # Given our transaction depth short circuits, we should be at the outermost loop,
420             # so it's safe to reset our variables.
421 17         346 $self->_retryable_timer( $self->_build_retryable_timer );
422              
423 17         2338 my $timeout = $self->_retryable_timer->timeout;
424 17 100       287 $timeout = 0 if $timeout == -1;
425 17         46 $self->_retryable_current_timeout($timeout);
426 17         60 $self->_retryable_call_type($call_type);
427              
428             # We have some post-processing to do, so save the BlockRunner object, and then save
429             # the result in a context-sensitive manner.
430 17         639 my $br = DBIx::Class::Storage::BlockRunner->new(
431             storage => $self,
432             wrap_txn => $call_type eq 'txn_do',
433              
434             # This neuters the max_attempts trigger in failed_attempt_count, so that the main check
435             # in our retry_handler works as expected.
436             max_attempts => 99999,
437              
438             retry_handler => \&_blockrunner_retry_handler,
439             );
440              
441             ### XXX: Outside exception handlers shouldn't interrupt the retry process, as it might never
442             ### never return back from the eval. This should really be a part of BlockRunner, but it's
443             ### not our module, so we hit the "local $SIG{__DIE__}" bit here. What that means is that
444             ### we're removing the die handler a bit too high up in the process, and we have exception
445             ### throwing that should use the outside handler.
446             ###
447             ### So, we save it here, and throw it out when we're done.
448              
449 17         4321 $self->_retryable_original_die_handler( $SIG{__DIE__} );
450              
451             return preserve_context {
452 17     17   251 local $SIG{__DIE__};
453 17         62 $br->run($target_runner);
454             }
455 17     15   514 after => sub { $self->_reset_timers_and_timeouts };
  15         9537684  
456              
457 0         0 $self->_retryable_original_die_handler(undef);
458             }
459              
460             # Our own BlockRunner retry handler
461             sub _blockrunner_retry_handler {
462 14     14   18424 my $br = shift;
463 14         61 my $self = $br->storage; # "self" for this module
464              
465 14         93 my $last_error = $br->last_exception;
466              
467             # Record the failure in the timer algorithm (prior to any checks)
468 14         551 my ($sleep_time, $new_timeout) = $self->_retryable_timer->failure;
469              
470             # If it's not a retryable error, stop here
471 14         2592 my $parsed_error = $self->parse_error_class->new($last_error);
472 14 100       5828 return $self->_reset_and_fail('Exception not transient') unless $parsed_error->is_transient;
473              
474 13         3530 $last_error =~ s/\n.+//s;
475 13 50       366 $self->_warn_retryable_error($last_error) if $self->warn_on_retryable_error;
476              
477             # Either stop here (because of timeout or max attempts), sleep, or don't
478 13 100       523 if ($sleep_time == -1) { return $self->_reset_and_fail('Out of retries') }
  1 100       7  
479 4         5558623 elsif ($sleep_time) { sleep $sleep_time; }
480              
481 12 50       116 if ($new_timeout > 0) {
482             # Reset the connection timeouts before we connect again
483 0         0 $self->_retryable_current_timeout($new_timeout);
484 0         0 $self->_set_dbi_connect_info;
485             }
486              
487             # Force a disconnect, but only if the connection seems to be in a broken state
488 12         39 local $@;
489 12 100       523 unless ($parsed_error->error_type eq 'lock') {
490 3         67 eval { local $SIG{__DIE__}; $self->disconnect };
  3         34  
  3         34  
491             }
492              
493             # Because BlockRunner calls this unprotected, and because our own _connect is going
494             # to hit the _in_do_block short-circuit, we should call this ourselves, in a
495             # protected eval, and re-direct any errors as if it was another failed attempt.
496 12         686 eval { $self->ensure_connected };
  12         106  
497 12 50       2245 if (my $connect_error = $@) {
498 0         0 push @{ $br->exception_stack }, $connect_error;
  0         0  
499 0         0 return _blockrunner_retry_handler($br);
500             }
501              
502 12         108 return 1;
503             }
504              
505             sub dbh_do {
506 9     9 1 37563 my $self = shift;
507 9 100       244 return $self->next::method(@_) unless $self->enable_retryable;
508 7         144 return $self->_blockrunner_do( dbh_do => @_ );
509             }
510              
511             #pod =head2 txn_do
512             #pod
513             #pod my $val = $schema->txn_do(
514             #pod sub {
515             #pod # ...DBIC calls within transaction...
516             #pod },
517             #pod @misc_args_passed_to_coderef,
518             #pod );
519             #pod
520             #pod Works just like L, except it's now protected against
521             #pod retryable failures.
522             #pod
523             #pod Calling this method through the C<$schema> object is typically more convenient.
524             #pod
525             #pod =cut
526              
527             sub txn_do {
528 1     1 1 1873 my $self = shift;
529 1 50       18 return $self->next::method(@_) unless $self->enable_retryable;
530              
531             # Connects or reconnects on pid change to grab correct txn_depth (same as
532             # DBIx::Class::Storage::DBI)
533 1         37 $self->_get_dbh;
534              
535 1         15 $self->_blockrunner_do( txn_do => @_ );
536             }
537              
538             ### XXX: This is a now deprecated method that only existed in the non-public version, but
539             ### it's a public method that should still exist for anybody previously using it.
540             sub is_dbi_error_retryable {
541 2     2 0 8988 my ($self, $error) = @_;
542 2         61 return $self->parse_error_class->new($error)->is_transient;
543             }
544              
545             #pod =head2 throw_exception
546             #pod
547             #pod $storage->throw_exception('It failed');
548             #pod
549             #pod Works just like L, but also reports attempt and
550             #pod timer statistics, in case the transaction was tried multiple times.
551             #pod
552             #pod =cut
553              
554             sub _reset_timers_and_timeouts {
555 17     17   50 my $self = shift;
556              
557             # Only reset timeouts if we have to, but check before we clear
558 17   66     61 my $needs_resetting = $self->_failed_attempt_count && $self->_retryable_current_timeout;
559              
560 17         102 $self->_retryable_timer(undef);
561 17         82 $self->_reset_retryable_timeout;
562              
563 17 50       57 if ($needs_resetting) {
564 0         0 $self->_set_dbi_connect_info;
565 0         0 $self->_set_retryable_session_timeouts;
566             }
567              
568             # Useful for chaining to the return call in _blockrunner_retry_handler
569 17         79 return undef;
570             }
571              
572             sub _warn_retryable_error {
573 0     0   0 my ($self, $error) = @_;
574              
575 0         0 my $timer = $self->_retryable_timer;
576 0         0 my $current_attempt_count = $self->_failed_attempt_count + 1;
577             my $debug_msg = sprintf(
578             'Retrying %s coderef, attempt %u of %u, timer: %.1f / %.1f sec, last exception: %s',
579             $self->_retryable_call_type,
580             $current_attempt_count, $self->max_attempts,
581             $timer->{_last_timestamp} - $timer->{_start_timestamp}, $timer->{max_actual_duration},
582 0         0 $error
583             );
584              
585 0         0 warn $debug_msg;
586             }
587              
588             sub _reset_and_fail {
589 2     2   431 my ($self, $fail_reason) = @_;
590              
591             # About to throw the main exception, so set the original handler
592 2         23 $SIG{__DIE__} = $self->_retryable_original_die_handler;
593              
594             # First error (by default): just pass the exception unaltered
595 2 50       13 if ($self->_failed_attempt_count <= $self->retries_before_error_prefix) {
596 0         0 $self->_retryable_exception_prefix(undef);
597 0         0 return $self->_reset_timers_and_timeouts;
598             }
599              
600 2         70 my $timer = $self->_retryable_timer;
601             $self->_retryable_exception_prefix( sprintf(
602             'Failed %s coderef: %s, attempts: %u / %u, timer: %.1f / %.1f sec',
603             $self->_retryable_call_type, $fail_reason,
604             $self->_failed_attempt_count, $self->max_attempts,
605             $timer->{_last_timestamp} - $timer->{_start_timestamp}, $timer->{max_actual_duration},
606 2         18 ) );
607              
608 2         15 return $self->_reset_timers_and_timeouts;
609             }
610              
611             sub throw_exception {
612 18     18 1 35511775 my $self = shift;
613              
614             # Clear the prefix as we use it
615 18         145 my $exception_prefix = $self->_retryable_exception_prefix;
616 18 100       454 $self->_retryable_exception_prefix(undef) if $exception_prefix;
617              
618 18 100       265 return $self->next::method(@_) unless $exception_prefix;
619              
620 2         36 my $error = shift;
621 2         9 $exception_prefix .= ', last exception: ';
622 2 50 33     47 if (blessed $error && $error->isa('DBIx::Class::Exception')) {
623 2         13 $error->{msg} = $exception_prefix.$error->{msg};
624             }
625             else {
626 0         0 $error = $exception_prefix.$error;
627             }
628 2         19 return $self->next::method($error, @_);
629             }
630              
631             #pod =head1 CAVEATS
632             #pod
633             #pod =head2 Transactions without txn_do
634             #pod
635             #pod Retryable is transaction-safe. Only the outermost transaction depth gets the retry
636             #pod protection, since that's the only layer that is idempotent and atomic.
637             #pod
638             #pod However, transaction commands like C and C are NOT granted
639             #pod retry protection, because DBIC/Retryable does not have a defined transaction-safe code
640             #pod closure to use upon reconnection. Only C will have the protections available.
641             #pod
642             #pod For example:
643             #pod
644             #pod # Has retry protetion
645             #pod my $rs = $schema->resultset('Foo');
646             #pod $rs->delete;
647             #pod
648             #pod # This effectively turns off retry protection
649             #pod $schema->txn_begin;
650             #pod
651             #pod # NOT protected from retryable errors!
652             #pod my $result = $rs->create({bar => 12});
653             #pod $result->update({baz => 42});
654             #pod
655             #pod $schema->txn_commit;
656             #pod # Retry protection is back on
657             #pod
658             #pod # Do this instead!
659             #pod $schema->txn_do(sub {
660             #pod my $result = $rs->create({bar => 12});
661             #pod $result->update({baz => 42});
662             #pod });
663             #pod
664             #pod # Still has retry protection
665             #pod $rs->delete;
666             #pod
667             #pod All of this behavior mimics how DBIC's original storage engines work.
668             #pod
669             #pod =head2 (Ab)using $dbh directly
670             #pod
671             #pod Similar to C, directly accessing and using a DBI database or statement handle
672             #pod does NOT grant retry protection, even if they are acquired from the storage engine via
673             #pod C<< $storage->dbh >>.
674             #pod
675             #pod Instead, use L. This method is also used by DBIC for most of its active DB
676             #pod calls, after it has composed a proper SQL statement to run.
677             #pod
678             #pod =head1 SEE ALSO
679             #pod
680             #pod L - A similar engine for DBI connections, using L as a base.
681             #pod
682             #pod L - Base module in DBIC that controls how transactional coderefs are ran and retried
683             #pod
684             #pod =cut
685              
686             1;
687              
688             __END__