File Coverage

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


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