Consolidate bits and pieces under ::_Util::refdesc
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / BlockRunner.pm
CommitLineData
9345b14c 1package # hide from pause until we figure it all out
2 DBIx::Class::Storage::BlockRunner;
3
59d624cf 4use strict;
5
9345b14c 6use DBIx::Class::Exception;
7use DBIx::Class::Carp;
8use Context::Preserve 'preserve_context';
841efcb3 9use DBIx::Class::_Util 'is_exception';
7d534e68 10use Scalar::Util qw(weaken blessed reftype);
9345b14c 11use Try::Tiny;
59d624cf 12
13# DO NOT edit away without talking to riba first, he will just put it back
14BEGIN {
15 local $ENV{PERL_STRICTURES_EXTRA} = 0;
16 require Moo; Moo->import;
17 require Sub::Quote; Sub::Quote->import('quote_sub');
18}
7d534e68 19use warnings NONFATAL => 'all';
9345b14c 20use namespace::clean;
21
22=head1 NAME
23
24DBIx::Class::Storage::BlockRunner - Try running a block of code until success with a configurable retry logic
25
26=head1 DESCRIPTION
27
28=head1 METHODS
29
30=cut
31
32has storage => (
33 is => 'ro',
34 required => 1,
35);
36
37has wrap_txn => (
38 is => 'ro',
39 required => 1,
40);
41
42# true - retry, false - rethrow, or you can throw your own (not catching)
43has retry_handler => (
44 is => 'ro',
45 required => 1,
7d534e68 46 isa => quote_sub( q{
47 (Scalar::Util::reftype($_[0])||'') eq 'CODE'
9345b14c 48 or DBIx::Class::Exception->throw('retry_handler must be a CODE reference')
7d534e68 49 }),
9345b14c 50);
51
52has retry_debug => (
53 is => 'rw',
7d534e68 54 # use a sub - to be evaluated on the spot lazily
9345b14c 55 default => quote_sub( '$ENV{DBIC_STORAGE_RETRY_DEBUG}' ),
7d534e68 56 lazy => 1,
9345b14c 57);
58
7d534e68 59has max_attempts => (
9345b14c 60 is => 'ro',
7d534e68 61 default => 20,
9345b14c 62);
63
7d534e68 64has failed_attempt_count => (
9345b14c 65 is => 'ro',
7d534e68 66 init_arg => undef, # ensures one can't pass the value in
67 writer => '_set_failed_attempt_count',
68 default => 0,
9345b14c 69 lazy => 1,
70 trigger => quote_sub(q{
f9080e45 71 $_[0]->throw_exception( sprintf (
7d534e68 72 'Reached max_attempts amount of %d, latest exception: %s',
73 $_[0]->max_attempts, $_[0]->last_exception
74 )) if $_[0]->max_attempts <= ($_[1]||0);
9345b14c 75 }),
76);
77
78has exception_stack => (
79 is => 'ro',
80 init_arg => undef,
81 clearer => '_reset_exception_stack',
82 default => quote_sub(q{ [] }),
83 lazy => 1,
84);
85
86sub last_exception { shift->exception_stack->[-1] }
87
f9080e45 88sub throw_exception { shift->storage->throw_exception (@_) }
89
9345b14c 90sub run {
91 my $self = shift;
92
9345b14c 93 $self->_reset_exception_stack;
7d534e68 94 $self->_set_failed_attempt_count(0);
95
96 my $cref = shift;
97
98 $self->throw_exception('run() requires a coderef to execute as its first argument')
99 if ( reftype($cref)||'' ) ne 'CODE';
100
9345b14c 101 my $storage = $self->storage;
102
7d534e68 103 return $cref->( @_ ) if (
104 $storage->{_in_do_block}
105 and
106 ! $self->wrap_txn
107 );
9345b14c 108
109 local $storage->{_in_do_block} = 1 unless $storage->{_in_do_block};
110
7d534e68 111 return $self->_run($cref, @_);
9345b14c 112}
113
114# this is the actual recursing worker
115sub _run {
7d534e68 116 # internal method - we know that both refs are strong-held by the
117 # calling scope of run(), hence safe to weaken everything
118 weaken( my $self = shift );
119 weaken( my $cref = shift );
9345b14c 120
7d534e68 121 my $args = @_ ? \@_ : [];
9345b14c 122
123 # from this point on (defined $txn_init_depth) is an indicator for wrap_txn
124 # save a bit on method calls
125 my $txn_init_depth = $self->wrap_txn ? $self->storage->transaction_depth : undef;
126 my $txn_begin_ok;
127
128 my $run_err = '';
129
9345b14c 130 return preserve_context {
131 try {
132 if (defined $txn_init_depth) {
7d534e68 133 $self->storage->txn_begin;
9345b14c 134 $txn_begin_ok = 1;
135 }
7d534e68 136 $cref->( @$args );
9345b14c 137 } catch {
138 $run_err = $_;
139 (); # important, affects @_ below
140 };
141 } replace => sub {
142 my @res = @_;
143
7d534e68 144 my $storage = $self->storage;
9345b14c 145 my $cur_depth = $storage->transaction_depth;
146
147 if (defined $txn_init_depth and $run_err eq '') {
148 my $delta_txn = (1 + $txn_init_depth) - $cur_depth;
149
150 if ($delta_txn) {
151 # a rollback in a top-level txn_do is valid-ish (seen in the wild and our own tests)
152 carp (sprintf
153 'Unexpected reduction of transaction depth by %d after execution of '
154 . '%s, skipping txn_commit()',
155 $delta_txn,
7d534e68 156 $cref,
9345b14c 157 ) unless $delta_txn == 1 and $cur_depth == 0;
158 }
159 else {
160 $run_err = eval { $storage->txn_commit; 1 } ? '' : $@;
161 }
162 }
163
164 # something above threw an error (could be the begin, the code or the commit)
841efcb3 165 if ( is_exception $run_err ) {
9345b14c 166
167 # attempt a rollback if we did begin in the first place
168 if ($txn_begin_ok) {
169 # some DBDs go crazy if there is nothing to roll back on, perform a soft-check
170 my $rollback_exception = $storage->_seems_connected
171 ? (! eval { $storage->txn_rollback; 1 }) ? $@ : ''
172 : 'lost connection to storage'
173 ;
174
175 if ( $rollback_exception and (
176 ! defined blessed $rollback_exception
177 or
178 ! $rollback_exception->isa('DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION')
179 ) ) {
180 $run_err = "Transaction aborted: $run_err. Rollback failed: $rollback_exception";
181 }
182 }
183
7d534e68 184 push @{ $self->exception_stack }, $run_err;
185
186 # this will throw if max_attempts is reached
187 $self->_set_failed_attempt_count($self->failed_attempt_count + 1);
9345b14c 188
189 # init depth of > 0 ( > 1 with AC) implies nesting - no retry attempt queries
190 $storage->throw_exception($run_err) if (
191 (
192 defined $txn_init_depth
193 and
194 # FIXME - we assume that $storage->{_dbh_autocommit} is there if
195 # txn_init_depth is there, but this is a DBI-ism
196 $txn_init_depth > ( $storage->{_dbh_autocommit} ? 0 : 1 )
7d534e68 197 ) or ! $self->retry_handler->($self)
9345b14c 198 );
199
9345b14c 200 # we got that far - let's retry
7d534e68 201 carp( sprintf 'Retrying %s (attempt %d) after caught exception: %s',
202 $cref,
203 $self->failed_attempt_count + 1,
9345b14c 204 $run_err,
7d534e68 205 ) if $self->retry_debug;
9345b14c 206
207 $storage->ensure_connected;
208 # if txn_depth is > 1 this means something was done to the
4a0eed52 209 # original $dbh, otherwise we would not get past the preceding if()
9345b14c 210 $storage->throw_exception(sprintf
211 'Unexpected transaction depth of %d on freshly connected handle',
212 $storage->transaction_depth,
213 ) if (defined $txn_init_depth and $storage->transaction_depth);
214
7d534e68 215 return $self->_run($cref, @$args);
9345b14c 216 }
217
218 return wantarray ? @res : $res[0];
219 };
220}
221
0c11ad0e 222=head1 AUTHOR AND CONTRIBUTORS
9345b14c 223
0c11ad0e 224See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
9345b14c 225
226=head1 LICENSE
227
228You may distribute this code under the same terms as Perl itself.
229
230=cut
231
2321;