File Coverage

blib/lib/DBIx/Class/Storage/DBI/mysql/Retryable.pm
Criterion Covered Total %
statement 161 193 83.4
branch 56 84 66.6
condition 16 27 59.2
subroutine 35 37 94.5
pod 4 7 57.1
total 272 348 78.1


line stmt bran cond sub pod time code
1             package DBIx::Class::Storage::DBI::mysql::Retryable;
2              
3 2     2   406389 use strict;
  2         12  
  2         55  
4 2     2   11 use warnings;
  2         4  
  2         53  
5              
6 2     2   3042 use DBI '1.630';
  2         35204  
  2         474  
7 2     2   19 use base qw< DBIx::Class::Storage::DBI::mysql >;
  2         5  
  2         1358  
8              
9 2     2   264663 use Algorithm::Backoff::RetryTimeouts;
  2         18451  
  2         79  
10 2     2   20 use Context::Preserve;
  2         6  
  2         127  
11 2     2   1015 use DBIx::ParseError::MySQL;
  2         185809  
  2         79  
12 2     2   19 use List::Util qw< min max >;
  2         5  
  2         170  
13 2     2   14 use Scalar::Util qw< blessed >;
  2         4  
  2         85  
14 2     2   11 use Storable qw< dclone >;
  2         5  
  2         88  
15 2     2   13 use Time::HiRes qw< time sleep >;
  2         4  
  2         17  
16 2     2   257 use namespace::clean;
  2         5  
  2         18  
17              
18             # ABSTRACT: MySQL-specific DBIC storage engine with retry support
19 2     2   622 use version;
  2         5  
  2         28  
20             our $VERSION = 'v1.0.0'; # 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 20652 my $self = shift;
181 4         156 my $opts = $self->timer_options;
182              
183 4 100       157 return $opts->{max_attempts} = $_[0] if @_;
184 2   100     91 return $opts->{max_attempts} // 8;
185             }
186              
187             sub retryable_timeout {
188 16     16 0 79278 my $self = shift;
189 16         511 my $opts = $self->timer_options;
190              
191 16 100       652 return $opts->{max_actual_duration} = $_[0] if @_;
192 14   100     67 return $opts->{max_actual_duration} // 50;
193             }
194              
195             sub disable_retryable {
196 4     4 0 529105 my $self = shift;
197 4 100       184 $self->enable_retryable( $_[0] ? 0 : 1 ) if @_;
    50          
198 4 100       249 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   85 my $self = shift;
207             return $self->timer_class->new(
208 37         1206 %{ dclone $self->timer_options }
  37         1997  
209             );
210             }
211              
212             sub _reset_retryable_timeout {
213 20     20   6829 my $self = shift;
214              
215             # Use a temporary timer to get the first timeout value
216 20         76 my $timeout = $self->_build_retryable_timer->timeout;
217 20 100       4344 $timeout = 0 if $timeout == -1;
218 20         97 $self->_retryable_current_timeout($timeout);
219             }
220              
221 21   50 21   211 sub _failed_attempt_count { shift->_retryable_timer->{_attempts} // 0 }
222              
223             # Constructor
224             sub new {
225 2     2 1 35174 my $self = shift->next::method(@_);
226              
227 2         1467 $self->_reset_retryable_timeout;
228              
229 2         369 $self;
230             }
231              
232             # Return the list of timeout strings to check
233             sub _timeout_set_list {
234 8     8   24 my ($self, $type) = @_;
235              
236 8         14 my @timeout_set;
237 8 100       46 if ($type eq 'dbi') {
    50          
238 3         11 @timeout_set = (qw< connect write >);
239 3 50       67 push @timeout_set, 'read' if $self->aggressive_timeouts;
240              
241 3         82 @timeout_set = map { "mysql_${_}_timeout" } @timeout_set;
  6         36  
242             }
243             elsif ($type eq 'session') {
244 5         16 @timeout_set = (qw< lock_wait innodb_lock_wait net_read net_write >);
245 5 50       99 push @timeout_set, 'wait' if $self->aggressive_timeouts;
246              
247 5         131 @timeout_set = map { "${_}_timeout" } @timeout_set;
  20         52  
248             }
249             else {
250 0         0 die "Unknown mysql timeout set: $type";
251             }
252              
253 8         53 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   1212 my $self = shift;
260 10 100 100     196 return $self->next::method unless $self->_retryable_current_timeout && $self->enable_retryable;
261              
262 3         97 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         27 %{ $self->next::method }, # inherit the other default attributes
  3         18  
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   7082 my $self = shift;
311 18         73 $self->_set_retryable_session_timeouts;
312 18         114 $self->next::method(@_);
313             }
314              
315             sub _set_retryable_session_timeouts {
316 18     18   55 my $self = shift;
317 18 100 100     247 return unless $self->_retryable_current_timeout && $self->enable_retryable;
318              
319 5         176 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         10 local $@;
327 5         10 eval {
328 5         13 my $dbh = $self->_dbh;
329 5 50       15 if ($dbh) {
330 5         17 $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       147 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   20514 my $self = shift;
351 16 100       422 return $self->next::method() unless $self->enable_retryable;
352             # next::can here to do mro calculations prior to sending to _blockrunner_do
353 14         407 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   235 my $self = shift;
382 22         53 my $call_type = shift;
383 22         60 my $run_target = shift;
384              
385             # See https://metacpan.org/release/DBIx-Class/source/lib/DBIx/Class/Storage/DBI.pm#L842
386 22 100       86 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   7359 if ($call_type eq 'txn_do') { $run_target->( @$args ); }
  1 100       5  
    50          
392 19         103 elsif ($call_type eq 'dbh_do') { $self->$run_target( $self->_get_dbh, @$args ); }
393 15         75 elsif ($call_type eq 'connect') { $self->$run_target( @$args ); }
394 0         0 else { die "Unknown call type: $call_type" }
395 22         163 };
396              
397             # Transaction depth short circuit (same as DBIx::Class::Storage::DBI)
398 22 100 66     244 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         382 $self->_retryable_timer( $self->_build_retryable_timer );
403              
404 17         2955 my $timeout = $self->_retryable_timer->timeout;
405 17 100       391 $timeout = 0 if $timeout == -1;
406 17         80 $self->_retryable_current_timeout($timeout);
407 17         81 $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         762 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   327 $br->run($target_runner);
424             }
425 17     15   5018 after => sub { $self->_reset_timers_and_timeouts };
  15         9545105  
426             }
427              
428             # Our own BlockRunner retry handler
429             sub _blockrunner_retry_handler {
430 15     15   17579 my $br = shift;
431 15         58 my $self = $br->storage; # "self" for this module
432              
433 15         75 my $last_error = $br->last_exception;
434              
435             # Record the failure in the timer algorithm (prior to any checks)
436 15         505 my ($sleep_time, $new_timeout) = $self->_retryable_timer->failure;
437              
438             # If it's not a retryable error, stop here
439 15         2314 my $parsed_error = $self->parse_error_class->new($last_error);
440 15 100       5416 return $self->_reset_and_fail('Exception not transient') unless $parsed_error->is_transient;
441              
442 14         3448 $last_error =~ s/\n.+//s;
443 14 50       397 $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       501 if ($sleep_time == -1) { return $self->_reset_and_fail('Out of retries') }
  1 100       41  
447 4         5559085 elsif ($sleep_time) { sleep $sleep_time; }
448              
449 13 50       138 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         46 local $@;
457 13 100       638 unless ($parsed_error->error_type eq 'lock') {
458 4         79 eval { local $SIG{__DIE__}; $self->disconnect };
  4         34  
  4         50  
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         998 eval { $self->ensure_connected };
  13         115  
465 13 50       2639 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         104 return 1;
471             }
472              
473             sub dbh_do {
474 9     9 1 45263 my $self = shift;
475 9 100       249 return $self->next::method(@_) unless $self->enable_retryable;
476 7         254 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 2389 my $self = shift;
497 1 50       25 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         44 $self->_get_dbh;
502              
503 1         24 $self->_blockrunner_do( txn_do => @_ );
504             }
505              
506             #pod =head2 throw_exception
507             #pod
508             #pod $storage->throw_exception('It failed');
509             #pod
510             #pod Works just like L, but also reports attempt and
511             #pod timer statistics, in case the transaction was tried multiple times.
512             #pod
513             #pod =cut
514              
515             sub _reset_timers_and_timeouts {
516 17     17   54 my $self = shift;
517              
518             # Only reset timeouts if we have to, but check before we clear
519 17   66     78 my $needs_resetting = $self->_failed_attempt_count && $self->_retryable_current_timeout;
520              
521 17         123 $self->_retryable_timer(undef);
522 17         85 $self->_reset_retryable_timeout;
523              
524 17 50       67 if ($needs_resetting) {
525 0         0 $self->_set_dbi_connect_info;
526 0         0 $self->_set_retryable_session_timeouts;
527             }
528              
529             # Useful for chaining to the return call in _blockrunner_retry_handler
530 17         80 return undef;
531             }
532              
533             sub _warn_retryable_error {
534 0     0   0 my ($self, $error) = @_;
535              
536 0         0 my $timer = $self->_retryable_timer;
537 0         0 my $current_attempt_count = $self->_failed_attempt_count + 1;
538             my $debug_msg = sprintf(
539             'Retrying %s coderef, attempt %u of %u, timer: %.1f / %.1f sec, last exception: %s',
540             $self->_retryable_call_type,
541             $current_attempt_count, $self->max_attempts,
542             $timer->{_last_timestamp} - $timer->{_start_timestamp}, $timer->{max_actual_duration},
543 0         0 $error
544             );
545              
546 0         0 warn $debug_msg;
547             }
548              
549             sub _reset_and_fail {
550 2     2   262 my ($self, $fail_reason) = @_;
551              
552             # First error: just pass the exception unaltered
553 2 50       10 if ($self->_failed_attempt_count <= 1) {
554 0         0 $self->_retryable_exception_prefix(undef);
555 0         0 return $self->_reset_timers_and_timeouts;
556             }
557              
558 2         9 my $timer = $self->_retryable_timer;
559             $self->_retryable_exception_prefix( sprintf(
560             'Failed %s coderef: %s, attempts: %u / %u, timer: %.1f / %.1f sec',
561             $self->_retryable_call_type, $fail_reason,
562             $self->_failed_attempt_count, $self->max_attempts,
563             $timer->{_last_timestamp} - $timer->{_start_timestamp}, $timer->{max_actual_duration},
564 2         13 ) );
565              
566 2         10 return $self->_reset_timers_and_timeouts;
567             }
568              
569             sub throw_exception {
570 19     19 1 38509331 my $self = shift;
571              
572             # Clear the prefix as we use it
573 19         171 my $exception_prefix = $self->_retryable_exception_prefix;
574 19 100       442 $self->_retryable_exception_prefix(undef) if $exception_prefix;
575              
576 19 100       267 return $self->next::method(@_) unless $exception_prefix;
577              
578 2         36 my $error = shift;
579 2         7 $exception_prefix .= ', last exception: ';
580 2 50 33     35 if (blessed $error && $error->isa('DBIx::Class::Exception')) {
581 2         11 $error->{msg} = $exception_prefix.$error->{msg};
582             }
583             else {
584 0         0 $error = $exception_prefix.$error;
585             }
586 2         12 return $self->next::method($error, @_);
587             }
588              
589             #pod =head1 CAVEATS
590             #pod
591             #pod =head2 Transactions without txn_do
592             #pod
593             #pod Retryable is transaction-safe. Only the outermost transaction depth gets the retry
594             #pod protection, since that's the only layer that is idempotent and atomic.
595             #pod
596             #pod However, transaction commands like C and C are NOT granted
597             #pod retry protection, because DBIC/Retryable does not have a defined transaction-safe code
598             #pod closure to use upon reconnection. Only C will have the protections available.
599             #pod
600             #pod For example:
601             #pod
602             #pod # Has retry protetion
603             #pod my $rs = $schema->resultset('Foo');
604             #pod $rs->delete;
605             #pod
606             #pod # This effectively turns off retry protection
607             #pod $schema->txn_begin;
608             #pod
609             #pod # NOT protected from retryable errors!
610             #pod my $result = $rs->create({bar => 12});
611             #pod $result->update({baz => 42});
612             #pod
613             #pod $schema->txn_commit;
614             #pod # Retry protection is back on
615             #pod
616             #pod # Do this instead!
617             #pod $schema->txn_do(sub {
618             #pod my $result = $rs->create({bar => 12});
619             #pod $result->update({baz => 42});
620             #pod });
621             #pod
622             #pod # Still has retry protection
623             #pod $rs->delete;
624             #pod
625             #pod All of this behavior mimics how DBIC's original storage engines work.
626             #pod
627             #pod =head2 (Ab)using $dbh directly
628             #pod
629             #pod Similar to C, directly accessing and using a DBI database or statement handle
630             #pod does NOT grant retry protection, even if they are acquired from the storage engine via
631             #pod C<< $storage->dbh >>.
632             #pod
633             #pod Instead, use L. This method is also used by DBIC for most of its active DB
634             #pod calls, after it has composed a proper SQL statement to run.
635             #pod
636             #pod =head1 SEE ALSO
637             #pod
638             #pod L - A similar engine for DBI connections, using L as a base.
639             #pod
640             #pod L - Base module in DBIC that controls how transactional coderefs are ran and retried
641             #pod
642             #pod =cut
643              
644             1;
645              
646             __END__