File Coverage

blib/lib/DBIx/RetryConnect.pm
Criterion Covered Total %
statement 85 105 80.9
branch 18 38 47.3
condition 11 23 47.8
subroutine 17 18 94.4
pod 0 2 0.0
total 131 186 70.4


line stmt bran cond sub pod time code
1             package DBIx::RetryConnect;
2             $DBIx::RetryConnect::VERSION = '0.002002';
3             =head1 NAME
4              
5             DBIx::RetryConnect - automatically retry DBI connect() with exponential backoff
6              
7             =head1 SYNOPSIS
8              
9             use DBIx::RetryConnect qw(Pg); # use default settings for all Pg connections
10              
11             use DBIx::RetryConnect Pg => sub { {} }; # same as above
12              
13             use DBIx::RetryConnect Pg => sub { # set these options for all Pg connections
14             return { total_delay => 300, verbose => 1, ... }
15             };
16              
17             use DBIx::RetryConnect Pg => sub { # set options dynamically for Pg connections
18             my ($drh, $dsn, $user, $password, $attrib) = @_;
19              
20             # return undef to not retry for this connection
21              
22             # don't retry unless we're connecting to a specific database
23             return undef if $dsn !~ /foo/;
24              
25             # don't retry for errors that don't include "server" in the message
26             return undef if $drh->errstr !~ /server/i;
27              
28             # or return a hash ref containing the retry options to use
29             return { ... };
30             };
31              
32             =head1 DESCRIPTION
33              
34             The DBIx::RetryConnect module arranges for failed DBI connection attempts to be
35             automatically and transparently retried for a period of time, with a growing
36             delay between each retry.
37              
38             As far as the application is concerned there's no change in behaviour.
39             Either the connection succeeds at once, succeeds sometime later after one or
40             more retries, or fails after one or more retries. It isn't aware of the retries.
41              
42             The DBIx::RetryConnect module works by loading and I the connect
43             method of the specified driver module. This allows it to work cleanly 'under the
44             covers' and thus avoid dealing with the complexities of C,
45             C, C etc. etc.
46              
47             =head2 Multiple Usage
48              
49             When DBIx::RetryConnect is used to configure a driver, the configuration is
50             added to a list of configurations for that driver.
51              
52             When a connection fails for that driver the list of configuration code refs is
53             checked to find the first code ref that returns a hash reference. That hash is
54             then used to configure the retry behaviour for that connection retry.
55              
56             =head2 Randomization
57              
58             Wherever the documentation talks about the duration of a delay the I
59             delay is a random value between 75% and 100% of this value. This randomization
60             helps avoid a "thundering herd" where many systems might attempt to reconnect
61             at the same time.
62              
63             =head2 Options
64              
65             =head3 total_delay
66              
67             The total time in seconds to spend retrying the connection before giving up
68             (default 30 seconds).
69              
70             This time is an approximation. The actual time spent may overshoot by at least
71             the value of L, plus whatever time the connection attempts themselves
72             may take.
73              
74             =head3 start_delay
75              
76             The duration in seconds of the initial delay after the initial connection
77             attempt failed (default 0.1).
78              
79             =head3 backoff_factor
80              
81             For each subsequent attempt while retrying a connection the delay duration,
82             which started with L, is multiplied by L.
83              
84             The default is 2, which provides the common "exponential backoff" behaviour.
85             See also L.
86              
87             =head3 max_delay
88              
89             The maximum duration, in seconds, for any individual retry delay. The default
90             is the value of L divided by 4. See also L.
91              
92             =head3 verbose
93              
94             Enable extra logging.
95              
96             1 - log each use of DBIx::RetryConnect module
97             2 - also log each connect failure
98             3 - also log each connect retry
99             4 - also log each connect retry with more timing details
100              
101             The default is the value of the C environment
102             variable if set, else 0.
103              
104             =cut
105              
106              
107 4     4   324573 use strict;
  4         22  
  4         206  
108 4     4   21 use warnings;
  4         15  
  4         201  
109              
110 4     4   86 use Carp qw(carp croak);
  4         4  
  4         371  
111 4     4   5823 use DBI;
  4         64661  
  4         1080  
112              
113             # proxy
114              
115             my %installed_dbd_configs; # Pg => [ {...}, ... ]
116              
117              
118             sub import {
119 2     2   25 my($exporter, @imports) = @_;
120              
121 2 50       7 croak "No drivers specified"
122             unless @imports;
123              
124 2         6 while (my $dbd = shift @imports) {
125 2 100 66 0   15 my $options = (@imports && ref $imports[0]) ? shift @imports : sub { {} };
  0         0  
126              
127 2 50 33     14 croak "$exporter $dbd argument must be a CODE reference, not $options"
128             if defined($options) && ref $options ne 'CODE';
129              
130 2 50       7 if ($ENV{DBIX_RETRYCONNECT_VERBOSE}) {
131 0 0       0 my $desc = (defined $options) ? "$options" : "default";
132 0         0 carp "$exporter installing $desc config for $dbd";
133             }
134              
135 2   50     11 my $configs = $installed_dbd_configs{$dbd} ||= [];
136 2         3 push @$configs, $options; # add to list of configs for this DBD
137              
138             # install the retry hook for this DBD if this is the first config
139 2 50       7 install_retry_connect($dbd, $configs) if @$configs == 1;
140             }
141              
142 2         3174 return;
143             }
144              
145              
146             sub install_retry_connect {
147 2     2 0 3 my ($dbd, $configs) = @_;
148              
149 2         14 DBI->install_driver($dbd);
150              
151 2         2519 my $connect_method = "DBD::${dbd}::dr::connect";
152              
153             ## no critic (ProhibitNoStrict)
154 4 50   4   38 my $orig_connect_subref = do { no strict 'refs'; *$connect_method{CODE} }
  4         5  
  4         739  
  2         4  
  2         10  
155             or croak "$connect_method not defined";
156              
157 2         3 my $retry_state_class = "DBIx::RetryConnect::RetryState";
158              
159 2 50 50     14 if (($ENV{DBIX_RETRYCONNECT_VERBOSE}||0) >= 2) {
160 0         0 carp __PACKAGE__." installing $retry_state_class hook into DBD::$dbd";
161             }
162              
163             my $retry_connect_subref = sub {
164              
165 3     3   1343 my $retry;
166 3         4 while (1) {
167              
168 9         141 my $dbh = $orig_connect_subref->(@_);
169 9 100       534 return $dbh if $dbh;
170              
171 6   66     26 $retry ||= do {
172 1         4 my $options = pick_retry_options_from_configs($configs, \@_);
173 1 50       4 return undef if not $options;
174 1         9 $retry_state_class->new($options, \@_);
175             };
176              
177 6 50       24 $retry->pause
178             or return undef;
179             }
180 2         10 };
181              
182 2         3 do {
183 4     4   19 no warnings 'redefine'; ## no critic (ProhibitNoWarnings)
  4         5  
  4         196  
184 4     4   16 no strict 'refs'; ## no critic (ProhibitNoStrict)
  4         5  
  4         429  
185 2         5 *$connect_method = $retry_connect_subref;
186             };
187              
188 2         8 return;
189             }
190              
191              
192             sub pick_retry_options_from_configs {
193 1     1 0 2 my ($configs, $connect_args) = @_;
194              
195 1         2 for my $config (@$configs) {
196 1         4 my $dynamic_config = $config->(@$connect_args);
197 1 50       2014 return $dynamic_config if $dynamic_config;
198             }
199              
200 0         0 return undef; # no config matched, so no retry behaviour
201             }
202              
203              
204             {
205             package DBIx::RetryConnect::RetryState; ## no critic (ProhibitMultiplePackages)
206             $DBIx::RetryConnect::RetryState::VERSION = '0.002002';
207 4     4   18 use Carp qw(carp croak);
  4         4  
  4         222  
208 4     4   1253 use Time::HiRes qw(usleep);
  4         2591  
  4         22  
209 4     4   3195 use Hash::Util qw(lock_keys);
  4         7562  
  4         19  
210              
211             sub new {
212 1     1   2 my ($class, $options, $connect_args) = @_;
213              
214 1   50     14 my $self = bless {
215             total_delay => 30,
216             start_delay => undef,
217             next_delay => undef,
218             max_delay => undef,
219             backoff_factor => 2,
220             verbose => $ENV{DBIX_RETRYCONNECT_VERBOSE} || 0,
221             connect_args => $connect_args,
222             } => $class;
223 1         11 lock_keys(%$self);
224              
225 1         15 $self->{$_} = $options->{$_} for keys %$options;
226              
227 1   50     8 $self->{next_delay} ||= $self->{start_delay} ||= 0.1; # 0.1 = 100ms
      33        
228 1   33     10 $self->{max_delay} ||= ($self->{total_delay} / 4);
229              
230 1 50       6 if ($self->{verbose} >= 2) {
231 0         0 my @ca = @{$self->{connect_args}};
  0         0  
232 0         0 local $self->{connect_args} = "$ca[0]->{Name}:$ca[1]"; # just the driver and dsn, hide password
233 0         0 my $kv = DBI::_concat_hash_sorted($self, "=", ", ", 1, undef); ## no critic (ProtectPrivateSubs)
234 0         0 carp "$class $kv";
235             }
236              
237 1         5 return $self;
238             }
239              
240             sub calculate_next_delay {
241 6     6   9 my $self = shift;
242              
243 6 50       36 return 0 if $self->{total_delay} <= 0;
244              
245 6 50       29 if ($self->{next_delay} > $self->{max_delay}) {
246 0         0 $self->{next_delay} = $self->{max_delay};
247             }
248              
249             # treat half the delay time as fixed and half as random
250             # this helps avoid a thundering-herd problem
251 6         77 my $this_delay = ($self->{next_delay} * 0.75) + rand($self->{next_delay} * 0.25);
252              
253 6 50       18 if ($self->{verbose} >= 3) {
254              
255 0         0 my $extra = "";
256 0 0       0 $extra = sprintf " [delay %.1fs, remaining %.1fs]",
257             $self->{next_delay}, $self->{total_delay}
258             if $self->{verbose} >= 4;
259              
260             # fudge %Carp::Internal so the carp shows a more useful caller
261 0         0 local $Carp::Internal{'DBI'} = 1; ## no critic (ProhibitPackageVars)
262 0         0 local $Carp::Internal{'DBIx::RetryConnect'} = 1; ## no critic (ProhibitPackageVars)
263 0         0 my ($drh, $dsn) = @{$self->{connect_args}};
  0         0  
264 0         0 my $errstr = $drh->errstr;
265 0 0       0 $errstr = "(undef errstr)" if not defined $errstr;
266 0         0 carp sprintf "DBIx::RetryConnect(%s:%s): sleeping for %.2gs after error: %s%s",
267             $drh->{Name}, $dsn, $this_delay, $errstr, $extra;
268             }
269              
270 6         14 $self->{total_delay} -= $this_delay; # track actual remaining time
271 6         12 $self->{next_delay} *= $self->{backoff_factor}; # backoff
272              
273 6         16 return $this_delay;
274             }
275              
276             sub pause {
277 6     6   14 my $self = shift;
278              
279 6         19 my $this_delay = $self->calculate_next_delay;
280              
281 6 50       16 return 0 if not $this_delay;
282              
283 6         4919818 usleep($this_delay * 1_000_000); # microseconds
284              
285 6         113 return 1;
286             }
287              
288             } # end of DBIx::RetryConnect::RetryState
289              
290             1;