Author: timbo
Date: Fri Apr 27 05:25:17 2007
New Revision: 9465

Modified:
   dbi/trunk/lib/DBD/Gofer.pm
   dbi/trunk/lib/DBD/Gofer/Transport/Base.pm
   dbi/trunk/lib/DBI/Gofer/Request.pm
   dbi/trunk/lib/DBI/Gofer/Response.pm
   dbi/trunk/lib/DBI/Gofer/Transport/Base.pm
   dbi/trunk/lib/DBI/Util/_accessor.pm
   dbi/trunk/t/86gofer_fail.t

Log:
Add automatic retries on failure - works very well.
Currently the response_needs_retransmit method only knows to retry 
DBI_GOFER_RANDOM_FAIL errors.
Add retry_limit=N to gofer dsn.


Modified: dbi/trunk/lib/DBD/Gofer.pm
==============================================================================
--- dbi/trunk/lib/DBD/Gofer.pm  (original)
+++ dbi/trunk/lib/DBD/Gofer.pm  Fri Apr 27 05:25:17 2007
@@ -266,8 +266,8 @@
         my $transport = $dbh->{go_transport}
             or return $dbh->set_err(1, "Not connected (no transport)");
 
-        my $response = $transport->transmit_request($request);
-        $response ||= $transport->receive_response;
+        my ($response, $retransmit_sub) = 
$transport->transmit_request($request);
+        $response ||= $transport->receive_response($request, $retransmit_sub);
         $dbh->{go_response} = $response
             or die "No response object returned by $transport";
 
@@ -584,8 +584,8 @@
         my $transport = $sth->{go_transport}
             or return $sth->set_err(1, "Not connected (no transport)");
 
-        my $response = $transport->transmit_request($request);
-        $response ||= $transport->receive_response;
+        my ($response, $retransmit_sub) = 
$transport->transmit_request($request);
+        $response ||= $transport->receive_response($request, $retransmit_sub);
         $sth->{go_response} = $response
             or die "No response object returned by $transport";
         $dbh->{go_response} = $response; # mainly for last_insert_id

Modified: dbi/trunk/lib/DBD/Gofer/Transport/Base.pm
==============================================================================
--- dbi/trunk/lib/DBD/Gofer/Transport/Base.pm   (original)
+++ dbi/trunk/lib/DBD/Gofer/Transport/Base.pm   Fri Apr 27 05:25:17 2007
@@ -19,6 +19,7 @@
     go_dsn
     go_url
     go_timeout
+    go_retry_limit
 ));
 
 
@@ -29,9 +30,10 @@
     my $to = $self->go_timeout;
 
     my $transmit_sub = sub {
+        $self->trace_msg("transmit_request\n");
         local $SIG{ALRM} = sub { die "TIMEOUT\n" } if $to;
 
-        my $info = eval {
+        my $response = eval {
             local $SIG{PIPE} = sub {
                 my $extra = ($! eq "Broken pipe") ? "" : " ($!)";
                 die "Unable to send request: Broken pipe$extra\n";
@@ -47,38 +49,93 @@
             return DBI::Gofer::Response->new({ err => 1, errstr => $@ });
         }
 
-        return undef;
+        return $response;
     };
 
-    my $response = $transmit_sub->();
+    my $response = $self->_transmit_request_with_retries($request, 
$transmit_sub);
+
+    $self->trace_msg("transmit_request is returing a response itself\n") if 
$response;
 
     return $response unless wantarray;
     return ($response, $transmit_sub);
 }
 
 
-sub receive_response {
-    my $self = shift;
+sub _transmit_request_with_retries {
+    my ($self, $request, $transmit_sub) = @_;
+    my $response;
+    do {
+        $response = $transmit_sub->();
+    } while ( $response && $self->response_needs_retransmit($response, 
$request) );
+    return $response;
+}
 
+
+sub receive_response {
+    my ($self, $request, $retransmit_sub) = @_;
     my $to = $self->go_timeout;
-    local $SIG{ALRM} = sub { die "TIMEOUT\n" } if $to;
 
-    my $response = eval {
-        alarm($to) if $to;
-        $self->receive_response_by_transport();
+    my $receive_sub = sub {
+        $self->trace_msg("receive_response\n");
+        local $SIG{ALRM} = sub { die "TIMEOUT\n" } if $to;
+
+        my $response = eval {
+            alarm($to) if $to;
+            $self->receive_response_by_transport();
+        };
+        alarm(0) if $to;
+
+        if ($@) {
+            return $self->transport_timedout("receive_response", $to)
+                if $@ eq "TIMEOUT\n";
+            return DBI::Gofer::Response->new({ err => 1, errstr => $@ });
+        }
+        return $response;
     };
-    alarm(0) if $to;
 
-    if ($@) {
-        return $self->transport_timedout("receive_response", $to)
-            if $@ eq "TIMEOUT\n";
-        return DBI::Gofer::Response->new({ err => 1, errstr => $@ });
-    }
+    my $response;
+    do {
+        $response = $receive_sub->();
+        if ($self->response_needs_retransmit($response, $request)) {
+            $response = $self->_transmit_request_with_retries($request, 
$retransmit_sub);
+            $response ||= $receive_sub->();
+        }
+    } while ( $self->response_needs_retransmit($response, $request) );
 
     return $response;
 }
 
 
+sub response_needs_retransmit {
+    my ($self, $response, $request) = @_;
+
+    my $err = $response->err
+        or return 0; # nothing wen't wrong
+
+    my $retry;
+    my $errstr = $response->errstr || '';
+
+    my $idempotent = 0; # XXX set to 1 for idempotent requests, ie selects
+
+    $retry = 1 if $errstr =~ m/fake error induced by DBI_GOFER_RANDOM_FAIL/;
+
+    if (!$retry) {
+        $self->trace_msg("response_needs_retransmit: response not suitable for 
retry\n");
+        return 0;
+    }
+    my $meta = $request->meta;
+    my $retry_count = ++$meta->{retry_count};
+    my $retry_limit = $self->go_retry_limit;
+    $retry_limit = 2 unless defined $retry_limit;
+    if ($retry_count > $retry_limit) {
+        $self->trace_msg("response_needs_retransmit: $retry_count is too many 
retries\n");
+        return 0;
+    }
+    $self->trace_msg("response_needs_retransmit: retry $retry_count\n");
+    return 1;
+}
+
+
 sub transport_timedout {
     my ($self, $method, $timeout) = @_;
     $timeout ||= $self->go_timeout;

Modified: dbi/trunk/lib/DBI/Gofer/Request.pm
==============================================================================
--- dbi/trunk/lib/DBI/Gofer/Request.pm  (original)
+++ dbi/trunk/lib/DBI/Gofer/Request.pm  Fri Apr 27 05:25:17 2007
@@ -25,6 +25,9 @@
     sth_method_calls
     sth_result_attr
 ));
+__PACKAGE__->mk_accessors_with(make_accessor_autoviv_hashref => qw(
+    meta
+));
 
 
 sub new {

Modified: dbi/trunk/lib/DBI/Gofer/Response.pm
==============================================================================
--- dbi/trunk/lib/DBI/Gofer/Response.pm (original)
+++ dbi/trunk/lib/DBI/Gofer/Response.pm Fri Apr 27 05:25:17 2007
@@ -33,6 +33,9 @@
     sth_resultsets
     warnings
 ));
+__PACKAGE__->mk_accessors_with(make_accessor_autoviv_hashref => qw(
+    meta
+));
 
 
 sub new {

Modified: dbi/trunk/lib/DBI/Gofer/Transport/Base.pm
==============================================================================
--- dbi/trunk/lib/DBI/Gofer/Transport/Base.pm   (original)
+++ dbi/trunk/lib/DBI/Gofer/Transport/Base.pm   Fri Apr 27 05:25:17 2007
@@ -64,9 +64,10 @@
         $self->_dump("freezing $self->{trace} ".ref($data), $data)
             if !$skip_trace and $self->trace;
 
-        my $header = $packet_header_text;
+        local $data->{meta}; # don't include _meta in serialization
         my $data = $self->{serializer_obj}->serializer($data);
-        $header.$data;
+
+        $packet_header_text . $data;
     };
     if ($@) {
         chomp $@;

Modified: dbi/trunk/lib/DBI/Util/_accessor.pm
==============================================================================
--- dbi/trunk/lib/DBI/Util/_accessor.pm (original)
+++ dbi/trunk/lib/DBI/Util/_accessor.pm Fri Apr 27 05:25:17 2007
@@ -19,10 +19,10 @@
 
 sub mk_accessors {
     my($self, @fields) = @_;
-    $self->_mk_accessors('make_accessor', @fields);
+    $self->mk_accessors_with('make_accessor', @fields);
 }
 
-sub _mk_accessors {
+sub mk_accessors_with {
     my($self, $maker, @fields) = @_;
     my $class = ref $self || $self;
 
@@ -47,7 +47,18 @@
     return sub {
         my $self = shift;
         return $self->{$field} unless @_;
-        $self->{$field} = (@_ == 1 ? $_[0] : [EMAIL PROTECTED]);
+        croak "Too many arguments to $field" if @_ > 1;
+        return $self->{$field} = shift;
+    };
+}
+
+sub make_accessor_autoviv_hashref {
+    my($class, $field) = @_;
+    return sub {
+        my $self = shift;
+        return $self->{$field} ||= {} unless @_;
+        croak "Too many arguments to $field" if @_ > 1;
+        return $self->{$field} = shift;
     };
 }
 

Modified: dbi/trunk/t/86gofer_fail.t
==============================================================================
--- dbi/trunk/t/86gofer_fail.t  (original)
+++ dbi/trunk/t/86gofer_fail.t  Fri Apr 27 05:25:17 2007
@@ -24,6 +24,9 @@
 
 plan 'no_plan';
 
+my $tmp;
+my $fails;
+
 # we'll use the null transport for simplicity and speed
 # and the rush policy to limit the number of interactions with the gofer 
executor
 
@@ -39,30 +42,55 @@
 ok $dbh_100;
 
 ok !eval { $dbh_100->do("set foo=1") }, 'do method should fail';
+ok $dbh_100->errstr, 'errstr should be set';
+ok $@, '$@ should be set';
+like $@, '/fake error induced by DBI_GOFER_RANDOM_FAIL/';
 like $dbh_100->errstr, '/DBI_GOFER_RANDOM_FAIL/', 'errstr should contain 
DBI_GOFER_RANDOM_FAIL';
+
 ok !$dbh_100->{go_response}->executed_flag_set, 'go_response executed flag 
should be false';
 
 is precentage_exceptions(200, sub { $dbh_100->do("set foo=1") }), 100;
 
-# --- 50% failure rate
+# XXX randomness can't be predicted, so it's just possible these will fail
+
+# --- 50% failure rate, with no retries
 
 $ENV{DBI_GOFER_RANDOM_FAIL} = "2,do"; # 50% failure (almost)
-my $dbh_50 = 
DBI->connect("dbi:Gofer:transport=null;policy=rush;dsn=dbi:ExampleP:", 0, 0, {
-    RaiseError => 1, PrintError => 0,
-});
-ok $dbh_50;
-my $fails = precentage_exceptions(200, sub { $dbh_50->do("set foo=1") });
+ok my $dbh_50r0 = dbi_connect("policy=rush;retry_limit=0");
+$fails = precentage_exceptions(200, sub { $dbh_50r0->do("set foo=1") });
 print "target approx 50% random failures, got $fails%\n";
-# XXX randomness can't be predicted, so it's just possible these will fail
 cmp_ok $fails, '>', 10, 'should fail about 50% of the time, but at least 10%';
 cmp_ok $fails, '<', 90, 'should fail about 50% of the time, but not more than 
90%';
 
+# --- 50% failure rate, with many retries (should yield low failure rate)
+
+$ENV{DBI_GOFER_RANDOM_FAIL} = "2,do"; # 50% failure (almost)
+ok my $dbh_50r5 = dbi_connect("policy=rush;retry_limit=5");
+$fails = precentage_exceptions(200, sub { $dbh_50r5->do("set foo=1") });
+print "target approx 5% random failures, got $fails%\n";
+cmp_ok $fails, '<', 20, 'should fail < 20%';
+
+# --- 10% failure rate, with many retries (should yield zero failure rate)
+
+$ENV{DBI_GOFER_RANDOM_FAIL} = "10,do";
+ok my $dbh_1r10 = dbi_connect("policy=rush;retry_limit=10");
+$fails = precentage_exceptions(200, sub { $dbh_1r10->do("set foo=1") });
+cmp_ok $fails, '<', 1, 'should fail < 1%';
+
+
 exit 0;
 
+sub dbi_connect {
+    my ($gdsn) = @_;
+    return DBI->connect("dbi:Gofer:transport=null;$gdsn;dsn=dbi:ExampleP:", 0, 
0, {
+        RaiseError => 1, PrintError => 0,
+    });
+}
+
 sub precentage_exceptions {
     my ($count, $sub) = @_;
     my $i = $count;
-    my $exceptions;
+    my $exceptions = 0;
     while ($i--) {
         eval { $sub->() };
         if ($@) {

Reply via email to