File Coverage

blib/lib/DBIx/Class/Storage/BlockRunner.pm
Criterion Covered Total %
statement 79 81 97.5
branch 32 40 80.0
condition 20 26 76.9
subroutine 15 16 93.7
pod 0 3 0.0
total 146 166 87.9


line stmt bran cond sub pod time code
1             package # hide from pause until we figure it all out
2             DBIx::Class::Storage::BlockRunner;
3              
4 271     271   2684 use warnings;
  271         908  
  271         9069  
5 271     271   1623 use strict;
  271         856  
  271         5582  
6              
7 271     271   1486 use DBIx::Class::Exception;
  271         941  
  271         6404  
8 271     271   1594 use DBIx::Class::Carp;
  271         916  
  271         2010  
9 271     271   129533 use Context::Preserve 'preserve_context';
  271         140550  
  271         17061  
10 271     271   2159 use DBIx::Class::_Util qw(is_exception qsub);
  271         921  
  271         14146  
11 271     271   10711 use Scalar::Util qw(weaken blessed reftype);
  271         1244  
  271         15468  
12 271     271   1807 use Try::Tiny;
  271         879  
  271         17011  
13 271     271   115860 use Moo;
  271         1329269  
  271         1772  
14 271     271   348938 use namespace::clean;
  271         1009  
  271         2814  
15              
16             =head1 NAME
17              
18             DBIx::Class::Storage::BlockRunner - Try running a block of code until success with a configurable retry logic
19              
20             =head1 DESCRIPTION
21              
22             =head1 METHODS
23              
24             =cut
25              
26             has storage => (
27             is => 'ro',
28             required => 1,
29             );
30              
31             has wrap_txn => (
32             is => 'ro',
33             required => 1,
34             );
35              
36             # true - retry, false - rethrow, or you can throw your own (not catching)
37             has retry_handler => (
38             is => 'ro',
39             required => 1,
40             isa => qsub q{
41             (Scalar::Util::reftype($_[0])||'') eq 'CODE'
42             or DBIx::Class::Exception->throw('retry_handler must be a CODE reference')
43             },
44             );
45              
46             has retry_debug => (
47             is => 'rw',
48             # use a sub - to be evaluated on the spot lazily
49             default => qsub '$ENV{DBIC_STORAGE_RETRY_DEBUG}',
50             lazy => 1,
51             );
52              
53             has max_attempts => (
54             is => 'ro',
55             default => 20,
56             );
57              
58             has failed_attempt_count => (
59             is => 'ro',
60             init_arg => undef, # ensures one can't pass the value in
61             writer => '_set_failed_attempt_count',
62             default => 0,
63             lazy => 1,
64             trigger => qsub q{
65             $_[0]->throw_exception( sprintf (
66             'Reached max_attempts amount of %d, latest exception: %s',
67             $_[0]->max_attempts, $_[0]->last_exception
68             )) if $_[0]->max_attempts <= ($_[1]||0);
69             },
70             );
71              
72             has exception_stack => (
73             is => 'ro',
74             init_arg => undef,
75             clearer => '_reset_exception_stack',
76             default => qsub q{ [] },
77             lazy => 1,
78             );
79              
80 0     0 0 0 sub last_exception { shift->exception_stack->[-1] }
81              
82 24     24 0 360 sub throw_exception { shift->storage->throw_exception (@_) }
83              
84             sub run {
85 45290     45290 0 2551028 my $self = shift;
86              
87 45290         859364 $self->_reset_exception_stack;
88 45290         893706 $self->_set_failed_attempt_count(0);
89              
90 45290         481228 my $cref = shift;
91              
92 45290 100 100     184952 $self->throw_exception('run() requires a coderef to execute as its first argument')
93             if ( reftype($cref)||'' ) ne 'CODE';
94              
95 45266         108583 my $storage = $self->storage;
96              
97             return $cref->( @_ ) if (
98             $storage->{_in_do_block}
99             and
100 45266 50 66     131851 ! $self->wrap_txn
101             );
102              
103 45266 100       145836 local $storage->{_in_do_block} = 1 unless $storage->{_in_do_block};
104              
105 45266         114000 return $self->_run($cref, @_);
106             }
107              
108             # this is the actual recursing worker
109             sub _run {
110             # internal method - we know that both refs are strong-held by the
111             # calling scope of run(), hence safe to weaken everything
112 45276     45276   146578 weaken( my $self = shift );
113 45276         121115 weaken( my $cref = shift );
114              
115 45276 100       112121 my $args = @_ ? \@_ : [];
116              
117             # from this point on (defined $txn_init_depth) is an indicator for wrap_txn
118             # save a bit on method calls
119 45276 100       122766 my $txn_init_depth = $self->wrap_txn ? $self->storage->transaction_depth : undef;
120 45276         68077 my $txn_begin_ok;
121              
122 45276         74733 my $run_err = '';
123              
124             return preserve_context {
125             try {
126 45276 100       2829920 if (defined $txn_init_depth) {
127 455         11467 $self->storage->txn_begin;
128 455         1700 $txn_begin_ok = 1;
129             }
130 45276         126696 $cref->( @$args );
131             } catch {
132 152         4453 $run_err = $_;
133 152         969 (); # important, affects @_ below
134 45276     45276   719666 };
135             } replace => sub {
136 45264     45264   6457404 my @res = @_;
137              
138 45264         134266 my $storage = $self->storage;
139 45264         110292 my $cur_depth = $storage->transaction_depth;
140              
141 45264 100 100     134565 if (defined $txn_init_depth and ! is_exception $run_err) {
142 314         969 my $delta_txn = (1 + $txn_init_depth) - $cur_depth;
143              
144 314 50       952 if ($delta_txn) {
145             # a rollback in a top-level txn_do is valid-ish (seen in the wild and our own tests)
146 0 0 0     0 carp (sprintf
147             'Unexpected reduction of transaction depth by %d after execution of '
148             . '%s, skipping txn_commit()',
149             $delta_txn,
150             $cref,
151             ) unless $delta_txn == 1 and $cur_depth == 0;
152             }
153             else {
154 314 50       966 $run_err = eval { $storage->txn_commit; 1 } ? '' : $@;
  314         2270  
  314         1594  
155             }
156             }
157              
158             # something above threw an error (could be the begin, the code or the commit)
159 45264 100       138645 if ( is_exception $run_err ) {
160              
161             # attempt a rollback if we did begin in the first place
162 152 100       822 if ($txn_begin_ok) {
163             # some DBDs go crazy if there is nothing to roll back on, perform a soft-check
164             my $rollback_exception = $storage->_seems_connected
165 130 100       699 ? (! eval { $storage->txn_rollback; 1 }) ? $@ : ''
  121 100       1146  
  10         46  
166             : 'lost connection to storage'
167             ;
168              
169 130 100 66     1718 if ( $rollback_exception and (
      100        
170             ! defined blessed $rollback_exception
171             or
172             ! $rollback_exception->isa('DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION')
173             ) ) {
174 10         41 $run_err = "Transaction aborted: $run_err. Rollback failed: $rollback_exception";
175             }
176             }
177              
178 152         470 push @{ $self->exception_stack }, $run_err;
  152         3983  
179              
180             # this will throw if max_attempts is reached
181 152         5867 $self->_set_failed_attempt_count($self->failed_attempt_count + 1);
182              
183             # init depth of > 0 ( > 1 with AC) implies nesting - no retry attempt queries
184             $storage->throw_exception($run_err) if (
185             (
186             defined $txn_init_depth
187             and
188             # FIXME - we assume that $storage->{_dbh_autocommit} is there if
189             # txn_init_depth is there, but this is a DBI-ism
190 152 50 100     7328 $txn_init_depth > ( $storage->{_dbh_autocommit} ? 0 : 1 )
    100 100        
191             ) or ! $self->retry_handler->($self)
192             );
193              
194             # we got that far - let's retry
195 11 50       229 carp( sprintf 'Retrying %s (attempt %d) after caught exception: %s',
196             $cref,
197             $self->failed_attempt_count + 1,
198             $run_err,
199             ) if $self->retry_debug;
200              
201 11         158 $storage->ensure_connected;
202             # if txn_depth is > 1 this means something was done to the
203             # original $dbh, otherwise we would not get past the preceding if()
204 10 50 66     63 $storage->throw_exception(sprintf
205             'Unexpected transaction depth of %d on freshly connected handle',
206             $storage->transaction_depth,
207             ) if (defined $txn_init_depth and $storage->transaction_depth);
208              
209 10         43 return $self->_run($cref, @$args);
210             }
211              
212 45112 100       151185 return wantarray ? @res : $res[0];
213 45276         329042 };
214             }
215              
216             =head1 FURTHER QUESTIONS?
217              
218             Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
219              
220             =head1 COPYRIGHT AND LICENSE
221              
222             This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
223             by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
224             redistribute it and/or modify it under the same terms as the
225             L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
226              
227             =cut
228              
229             1;