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 267     267   1572 use warnings;
  267         548  
  267         7532  
5 267     267   1078 use strict;
  267         498  
  267         4780  
6              
7 267     267   1107 use DBIx::Class::Exception;
  267         477  
  267         5268  
8 267     267   986 use DBIx::Class::Carp;
  267         518  
  267         1354  
9 267     267   117089 use Context::Preserve 'preserve_context';
  267         104352  
  267         14800  
10 267     267   1455 use DBIx::Class::_Util qw(is_exception qsub);
  267         575  
  267         12680  
11 267     267   1568 use Scalar::Util qw(weaken blessed reftype);
  267         571  
  267         13511  
12 267     267   1208 use Try::Tiny;
  267         511  
  267         11825  
13 267     267   104510 use Moo;
  267         1118833  
  267         1434  
14 267     267   283062 use namespace::clean;
  267         612  
  267         2337  
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 216 sub throw_exception { shift->storage->throw_exception (@_) }
83              
84             sub run {
85 44771     44771 0 1841062 my $self = shift;
86              
87 44771         765004 $self->_reset_exception_stack;
88 44771         726991 $self->_set_failed_attempt_count(0);
89              
90 44771         335298 my $cref = shift;
91              
92 44771 100 100     169586 $self->throw_exception('run() requires a coderef to execute as its first argument')
93             if ( reftype($cref)||'' ) ne 'CODE';
94              
95 44747         62528 my $storage = $self->storage;
96              
97             return $cref->( @_ ) if (
98             $storage->{_in_do_block}
99             and
100 44747 50 66     113438 ! $self->wrap_txn
101             );
102              
103 44747 100       115604 local $storage->{_in_do_block} = 1 unless $storage->{_in_do_block};
104              
105 44747         84178 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 44757     44757   92632 weaken( my $self = shift );
113 44757         73632 weaken( my $cref = shift );
114              
115 44757 100       82253 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 44757 100       89456 my $txn_init_depth = $self->wrap_txn ? $self->storage->transaction_depth : undef;
120 44757         37004 my $txn_begin_ok;
121              
122 44757         47097 my $run_err = '';
123              
124             return preserve_context {
125             try {
126 44757 100       1863333 if (defined $txn_init_depth) {
127 455         11917 $self->storage->txn_begin;
128 455         636 $txn_begin_ok = 1;
129             }
130 44757         105463 $cref->( @$args );
131             } catch {
132 153         3651 $run_err = $_;
133 153         815 (); # important, affects @_ below
134 44757     44757   494133 };
135             } replace => sub {
136 44745     44745   5594617 my @res = @_;
137              
138 44745         92692 my $storage = $self->storage;
139 44745         71536 my $cur_depth = $storage->transaction_depth;
140              
141 44745 100 100     108594 if (defined $txn_init_depth and ! is_exception $run_err) {
142 314         722 my $delta_txn = (1 + $txn_init_depth) - $cur_depth;
143              
144 314 50       1038 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       553 $run_err = eval { $storage->txn_commit; 1 } ? '' : $@;
  314         1581  
  314         1215  
155             }
156             }
157              
158             # something above threw an error (could be the begin, the code or the commit)
159 44745 100       111601 if ( is_exception $run_err ) {
160              
161             # attempt a rollback if we did begin in the first place
162 153 100       572 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       452 ? (! eval { $storage->txn_rollback; 1 }) ? $@ : ''
  121 100       773  
  10         39  
166             : 'lost connection to storage'
167             ;
168              
169 130 100 66     1576 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         32 $run_err = "Transaction aborted: $run_err. Rollback failed: $rollback_exception";
175             }
176             }
177              
178 153         330 push @{ $self->exception_stack }, $run_err;
  153         4176  
179              
180             # this will throw if max_attempts is reached
181 153         5104 $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 153 50 100     6852 $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       202 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         112 $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     56 $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         36 return $self->_run($cref, @$args);
210             }
211              
212 44592 100       122261 return wantarray ? @res : $res[0];
213 44757         297879 };
214             }
215              
216             =head1 FURTHER QUESTIONS?
217              
218             Check the list of L.
219              
220             =head1 COPYRIGHT AND LICENSE
221              
222             This module is free software L
223             by the L. You can
224             redistribute it and/or modify it under the same terms as the
225             L.
226              
227             =cut
228              
229             1;