DBIx-Connector-Retry
view release on metacpan or search on metacpan
lib/DBIx/Connector/Retry.pm view on Meta::CPAN
package DBIx::Connector::Retry;
our $AUTHORITY = 'cpan:GSG';
# ABSTRACT: DBIx::Connector with block retry support
use version;
our $VERSION = 'v0.900.3'; # VERSION
use strict;
use warnings;
use Moo;
extends 'DBIx::Connector', 'Moo::Object';
use Scalar::Util qw( weaken );
use Types::Standard qw( Str Bool HashRef CodeRef Dict Tuple Optional Maybe );
use Types::Common::Numeric qw( PositiveInt );
use namespace::clean; # don't export the above
#pod =encoding utf8
#pod
#pod =head1 SYNOPSIS
#pod
#pod my $conn = DBIx::Connector::Retry->new(
#pod connect_info => [ 'dbi:Driver:database=foobar', $user, $pass, \%args ],
#pod retry_debug => 1,
#pod max_attempts => 5,
#pod );
#pod
#pod # Keep retrying/reconnecting on errors
#pod my ($count) = $conn->run(ping => sub {
#pod $_->do('UPDATE foobar SET updated = 1 WHERE active = ?', undef, 'on');
#pod $_->selectrow_array('SELECT COUNT(*) FROM foobar WHERE updated = 1');
#pod });
#pod
#pod # Add a simple retry_handler for a manual timeout
#pod my $start_time = time;
#pod $conn->retry_handler(sub { time <= $start_time + 60 });
#pod
#pod my ($count) = $conn->txn(fixup => sub {
#pod $_->selectrow_array('SELECT COUNT(*) FROM barbaz');
#pod });
#pod $conn->clear_retry_handler;
#pod
#pod # Plus everything else in DBIx::Connector
#pod
#pod =head1 DESCRIPTION
#pod
#pod DBIx::Connector::Retry is a Moo-based subclass of L<DBIx::Connector> that will retry on
#pod failures. Most of the interface was modeled after L<DBIx::Class::Storage::BlockRunner>
#pod and adapted for use in DBIx::Connector.
#pod
#pod =head1 ATTRIBUTES
#pod
#pod =head2 connect_info
#pod
#pod An arrayref that contains all of the connection details normally found in the L<DBI> or
#pod L<DBIx::Connector> call. This data can be changed, but won't take effect until the next
#pod C<$dbh> re-connection cycle.
#pod
#pod Obviously, this is required.
#pod
#pod =cut
has connect_info => (
is => 'rw',
# Yes, DBI->connect() is still technically-valid syntax
isa => Tuple[ Maybe[Str], Maybe[Str], Maybe[Str], Optional[HashRef] ],
required => 1,
);
#pod =head2 mode
#pod
#pod This is just like L<DBIx::Connector/mode> except that it can be set from within the
lib/DBIx/Connector/Retry.pm view on Meta::CPAN
#pod );
#pod
#pod As this is a L<Moo> class, it uses the standard Moo constructor. The L</connect_info>
#pod should be specified as its own key. The L<DBI>/L<DBIx::Connector> syntax is available,
#pod but only as a nicety for compatibility.
#pod
#pod =cut
around BUILDARGS => sub {
my ($orig, $class, @args) = @_;
# Old-style DBI/DBIx::Connector parameters. Try to fix it up.
if (@args && $args[0] && !ref $args[0] && $args[0] =~ /^dbi:/) {
my @connect_info = splice(@args, 0, 3); # DBI DSN, UN, PW
push @connect_info, shift @args if $args[0] && (ref $args[0]||'') eq 'HASH'; # DBI \%attr, if it exists
if ( @args && $args[0] && (my $ref = ref $args[0]) ) {
if ($ref eq 'ARRAY') {
push @{$args[0]}, ( connect_info => \@connect_info );
@args = @{$args[0]}; # Moo::Object::BUILDARGS doesn't actually support lone ARRAYREFs
}
elsif ($ref eq 'HASH') {
$args[0]{connect_info} = \@connect_info;
}
else {
# Mimicing Moo::Object::BUILDARGS here
Carp::croak(join ' ',
"The new() method for $class cannot parse the strange argument list.",
"Please switch to a standard Moo constructor, instead of the DBI syntax.",
);
}
}
else {
# either the key within a list or we're out of arguments
push @args, ( connect_info => \@connect_info );
}
}
return $class->$orig(@args);
};
sub BUILD {
my ($self, $args) = @_;
my @connect_args = @{ $self->connect_info };
# Add in the keys that DBIx::Connector expects. For the purposes of future
# expandability of DBIx::Connector, we do this by getting a new base Connector
# object, and inject those properties into our own object.
my $base_obj = DBIx::Connector->new(@connect_args);
%$self = (
%$base_obj,
%$self, # $self's existing attributes take priority
);
# DBIx::Connector stores connection details in a coderef (for some reason). Instead
# of just dumping the same arguments as another copy, we'll tie it directly to the
# attr. If connect_info ever changes, it will grab the latest version.
$self->{_args} = sub { @{ $self->connect_info } };
weaken $self; # circular closure ref
}
#pod =head1 MODIFIED METHODS
#pod
#pod =head2 run / txn
#pod
#pod my @result = $conn->run($mode => $coderef);
#pod my $result = $conn->run($mode => $coderef);
#pod $conn->run($mode => $coderef);
#pod
#pod my @result = $conn->txn($mode => $coderef);
#pod my $result = $conn->txn($mode => $coderef);
#pod $conn->txn($mode => $coderef);
#pod
#pod Both L<run|DBIx::Connector/run> and L<txn|DBIx::Connector/txn> are modified to run inside
#pod a retry loop. If the original Connector action dies, the exception is caught, and if
#pod L</retry_handler> and L</max_attempts> allows it, the action is retried. The database
#pod handle may be reset by the Connector action, according to its connection mode.
#pod
#pod See L</CAVEATS> for important behaviors/limitations.
#pod
#pod =cut
foreach my $method (qw< run txn >) {
around $method => sub {
my $orig = shift;
my $self = shift;
my $mode = ref $_[0] eq 'CODE' ? $self->{_mode} : shift;
my $cref = shift;
my $wantarray = wantarray;
return $self->_retry_loop($orig, $method, $mode, $cref, $wantarray);
};
}
sub _retry_loop {
my ($self, $orig, $method, $mode, $cref, $wantarray) = @_;
# For the purposes of nesting, these variables should be localized.
local $self->{exception_stack} = [];
local $self->{failed_attempt_count} = 0;
local $self->{execute_method} = $method;
# If we already started in a transaction, that implies nesting, so don't
# retry the query. We can't guarantee that the statements before the block
# run will be committed, and are assuming that the connection will break.
#
# We cannot rely on checking the database connection via ping, because some
# DBDs (like mysql) will try to reconnect to the DB if the first ping check
# fails, and a reconnection auto-rollbacks all transactions, locks, etc.
if ($self->in_txn) {
unless (defined $wantarray) { return $self->$orig($mode, $cref) }
elsif ($wantarray) { return ($self->$orig($mode, $cref)) }
else { return scalar $self->$orig($mode, $cref) }
}
# Mode is localized within $orig, but we should localize it again ourselves, in case
# it's changed on-the-fly.
local $self->{_mode} = $mode;
( run in 1.013 second using v1.01-cache-2.11-cpan-5735350b133 )