Author: timbo
Date: Fri Sep 28 09:11:07 2007
New Revision: 10015

Added:
   dbi/trunk/lib/DBI/Util/Cache.pm   (contents, props changed)
   dbi/trunk/t/87gofer_cache.t
Modified:
   dbi/trunk/Changes
   dbi/trunk/MANIFEST
   dbi/trunk/Makefile.PL
   dbi/trunk/lib/DBD/Gofer/Transport/Base.pm
   dbi/trunk/lib/DBD/Gofer/Transport/null.pm
   dbi/trunk/lib/DBD/Gofer/Transport/stream.pm
   dbi/trunk/lib/DBI/Gofer/Transport/Base.pm

Log:
  Added client-side caching to DBD::Gofer. Can use any cache
    object compatible with the Cache module interface.
  Added DBI::Util::Cache for use with DBD::Gofer
    It's a very fast and small strict subset of Cache::Memory.


Modified: dbi/trunk/Changes
==============================================================================
--- dbi/trunk/Changes   (original)
+++ dbi/trunk/Changes   Fri Sep 28 09:11:07 2007
@@ -48,6 +48,11 @@
     thanks to Jerry D. Hedden and Michael G Schwern.
   Fixed DBI for VMS thanks to Peter (Stig) Edwards.
 
+  Added client-side caching to DBD::Gofer. Can use any cache
+    object compatible with the Cache module interface.
+  Added DBI::Util::Cache for use with DBD::Gofer
+    It's a very fast and small strict subset of Cache::Memory.
+
 =head2 Changes in DBI 1.59 (svn rev 9874),  23rd August 2007
 
   Fixed DBI::ProfileData to unescape headers lines read from data file.

Modified: dbi/trunk/MANIFEST
==============================================================================
--- dbi/trunk/MANIFEST  (original)
+++ dbi/trunk/MANIFEST  Fri Sep 28 09:11:07 2007
@@ -62,7 +62,8 @@
 lib/DBI/ProxyServer.pm         The proxy drivers server
 lib/DBI/PurePerl.pm            A DBI.xs emulation in Perl
 lib/DBI/SQL/Nano.pm            A 'smaller than micro' SQL parser
-lib/DBI/Util/_accessor.pm       A cut-down version of Class::Accessor::Fast
+lib/DBI/Util/_accessor.pm       A very�cut-down version of 
Class::Accessor::Fast
+lib/DBI/Util/Cache.pm           A very cut-down version of Cache::Memory
 lib/DBI/W32ODBC.pm             An experimental DBI emulation layer for 
Win32::ODBC
 lib/Win32/DBIODBC.pm           An experimental Win32::ODBC emulation layer for 
DBI
 t/01basics.t
@@ -95,6 +96,7 @@
 t/80proxy.t
 t/85gofer.t
 t/86gofer_fail.t
+t/87gofer_cache.t
 t/pod.t
 test.pl                        Assorted informal tests, including tests for 
memory leaks
 typemap

Modified: dbi/trunk/Makefile.PL
==============================================================================
--- dbi/trunk/Makefile.PL       (original)
+++ dbi/trunk/Makefile.PL       Fri Sep 28 09:11:07 2007
@@ -139,7 +139,7 @@
     EXE_FILES => [ "dbiproxy$ext_pl", "dbiprof$ext_pl", "dbilogstrip$ext_pl" ],
     DIR => [ ],
     dynamic_lib => { OTHERLDFLAGS => "$::opt_g" },
-    clean => { FILES=> "\$(DISTVNAME) Perl.xsi t/zv*_*.t"
+    clean => { FILES=> "\$(DISTVNAME) Perl.xsi t/zv*_*.t dbi__null_test_tmp*"
                 ." dbiproxy$ext_pl dbiprof$ext_pl dbilogstrip$ext_pl 
dbitrace.log dbi.prof ndtest.prt" },
     dist  => {
        DIST_DEFAULT=> 'clean distcheck disttest tardist',

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 Sep 28 09:11:07 2007
@@ -22,12 +22,23 @@
     go_timeout
     go_retry_hook
     go_retry_limit
+    go_cache
+    cache_hit
+    cache_miss
+    cache_store
 ));
 __PACKAGE__->mk_accessors_using(make_accessor_autoviv_hashref => qw(
     meta
 ));
 
 
+sub new {
+    my ($class, $args) = @_;
+    $args->{$_} = 0 for (qw(cache_hit cache_miss cache_store));
+    return $class->SUPER::new($args);
+}   
+
+
 sub _init_trace { $ENV{DBD_GOFER_TRACE} || 0 }
 
 
@@ -39,8 +50,24 @@
 
 sub transmit_request {
     my ($self, $request) = @_;
-    my $to = $self->go_timeout;
+    my $response;
+
+    if (my $go_cache = $self->{go_cache}) {
+        my $request_key = $self->get_cache_key_for_request($request);
+        my $frozen_response = $go_cache->get($request_key) if $request_key;
+        if ($frozen_response) {
+            $response = $self->thaw_response($frozen_response);
+            my $trace = $self->trace;
+            $self->_dump("cached response found for ".ref($request), $request) 
if $trace;
+            $self->_dump("cached response is ".ref($response), $response) if 
$trace;
+            $self->trace_msg("transmit_request is returing a response from 
cache\n");
+            ++$self->{cache_hit};
+            return $response;
+        }
+        ++$self->{cache_miss} if $request_key;
+    }
 
+    my $to = $self->go_timeout;
     my $transmit_sub = sub {
         $self->trace_msg("transmit_request\n");
         local $SIG{ALRM} = sub { die "TIMEOUT\n" } if $to;
@@ -64,7 +91,7 @@
         return $response;
     };
 
-    my $response = $self->_transmit_request_with_retries($request, 
$transmit_sub);
+    $response = $self->_transmit_request_with_retries($request, $transmit_sub);
 
     $self->trace_msg("transmit_request is returing a response itself\n") if 
$response;
 
@@ -93,7 +120,7 @@
 
         my $response = eval {
             alarm($to) if $to;
-            $self->receive_response_by_transport();
+            $self->receive_response_by_transport($request);
         };
         alarm(0) if $to;
 
@@ -114,6 +141,18 @@
         }
     } while ( $self->response_needs_retransmit($request, $response) );
 
+    my $frozen_response = delete $response->{meta}{frozen}
+        or warn "No meta frozen in request";
+
+    if ($frozen_response and my $go_cache = $self->{go_cache}) {
+        my $request_key = $self->get_cache_key_for_request($request);
+        if ($request_key) {
+            $self->trace_msg("receive_response added response to cache\n");
+            $go_cache->set($request_key, $frozen_response);
+            ++$self->{cache_store};
+        }
+    }
+
     return $response;
 }
 
@@ -173,6 +212,23 @@
 }
 
 
+# return undef if we don't want to cache this request
+sub get_cache_key_for_request {
+    my ($self, $request) = @_;
+
+    # we only want to cache idempotent requests
+    # is_idempotent() is true if GOf_REQUEST_IDEMPOTENT or 
GOf_REQUEST_READONLY set
+    return undef if not $request->is_idempotent;
+
+    # XXX would be nice to avoid the extra freeze here
+    my $key = $self->freeze_request($request, undef, 1);
+
+    #use Digest::MD5; warn "get_cache_key_for_request: 
".Digest::MD5::md5_base64($key)."\n";
+
+    return $key;
+}
+
+
 1;
 
 =head1 NAME

Modified: dbi/trunk/lib/DBD/Gofer/Transport/null.pm
==============================================================================
--- dbi/trunk/lib/DBD/Gofer/Transport/null.pm   (original)
+++ dbi/trunk/lib/DBD/Gofer/Transport/null.pm   Fri Sep 28 09:11:07 2007
@@ -18,6 +18,7 @@
 
 __PACKAGE__->mk_accessors(qw(
     pending_response
+    transmit_count
 )); 
 
 my $executor = DBI::Gofer::Execute->new();
@@ -25,6 +26,7 @@
 
 sub transmit_request_by_transport {
     my ($self, $request) = @_;
+    $self->transmit_count( ($self->transmit_count()||0) + 1 ); # just for tests
 
     my $frozen_request = $self->freeze_request($request);
 

Modified: dbi/trunk/lib/DBD/Gofer/Transport/stream.pm
==============================================================================
--- dbi/trunk/lib/DBD/Gofer/Transport/stream.pm (original)
+++ dbi/trunk/lib/DBD/Gofer/Transport/stream.pm Fri Sep 28 09:11:07 2007
@@ -148,7 +148,7 @@
     my ($pid, $rfh, $efh, $cmd) = @{$connection}{qw(pid rfh efh cmd)};
 
     my $errno = 0;
-    my $frozen_response;
+    my $encoded_response;
     my $stderr_msg;
 
     $self->read_response_from_fh( {
@@ -160,14 +160,14 @@
         $rfh => {
             error => sub { warn "error reading response: $!"; $errno||=$!; 1 },
             eof   => sub { warn "eof on stdout" if 0; 1 },
-            read  => sub { $frozen_response .= $_; 
($frozen_response=~s/\015\012$//) ? 1 : 0 },
+            read  => sub { $encoded_response .= $_; 
($encoded_response=~s/\015\012$//) ? 1 : 0 },
         },
     });
 
     # if we got no output on stdout at all then the command has
     # probably exited, possibly with an error to stderr.
     # Turn this situation into a reasonably useful DBI error.
-    if (not $frozen_response) {
+    if (not $encoded_response) {
         my $msg = "No response received";
         if (chomp $stderr_msg && $stderr_msg) {
             $msg .= sprintf ", error reported by \"%s\" (pid %d%s): %s",
@@ -181,14 +181,16 @@
         die "$msg\n";
     }
 
-    $self->trace_msg("Response received: $frozen_response\n",0)
+    $self->trace_msg("Response received: $encoded_response\n",0)
         if $trace >= 4;
 
     $self->trace_msg("Gofer stream stderr message: $stderr_msg\n",0)
         if $stderr_msg && $trace;
 
+    my $frozen_response = pack("H*", $encoded_response);
+
     # XXX need to be able to detect and deal with corruption
-    my $response = $self->thaw_response(pack("H*",$frozen_response));
+    my $response = $self->thaw_response($frozen_response);
 
     if ($stderr_msg) {
         # add stderr messages as warnings (for PrintWarn)

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 Sep 28 09:11:07 2007
@@ -23,6 +23,7 @@
 
 __PACKAGE__->mk_accessors(qw(
     trace
+    keep_meta_frozen
     serializer_obj
 ));
 
@@ -35,6 +36,7 @@
     my ($class, $args) = @_;
     $args->{trace} ||= $class->_init_trace;
     $args->{serializer_obj} ||= DBI::Gofer::Serializer::Storable->new();
+    $args->{keep_meta_frozen} ||= 1 if $args->{go_cache};
     my $self = bless {}, $class;
     $self->$_( $args->{$_} ) for keys %$args;
     $self->trace_msg("$class->new({ @{[ %$args ]} })\n") if $self->trace;
@@ -61,6 +63,11 @@
         chomp $@;
         die "Error freezing ".ref($data)." object: $@";
     }
+
+    # stash the frozen data into the data structure itself
+    # to make life easy for the client caching code in 
DBD::Gofer::Transport::Base
+    $data->{meta}{frozen} = $frozen if $self->keep_meta_frozen;
+
     return $frozen;
 }
 # public aliases used by subclasses
@@ -73,14 +80,16 @@
     my $data;
     eval {
         # check for and extract our gofer header and the info it contains
-        $frozen_data =~ s/$packet_header_regex//o
+        (my $frozen = $frozen_data) =~ s/$packet_header_regex//o
             or die "does not have gofer header\n";
         my ($t_version) = $1;
        $serializer ||= $self->{serializer_obj};
-        $data = $serializer->deserialize($frozen_data);
+        $data = $serializer->deserialize($frozen);
         die ref($serializer)."->deserialize didn't return a reference"
             unless ref $data;
         $data->{_transport}{version} = $t_version;
+
+        $data->{meta}{frozen} = $frozen_data if $self->keep_meta_frozen;
     };
     if ($@) {
         chomp(my $err = $@);
@@ -92,6 +101,7 @@
     }
     $self->_dump("thawing $self->{trace} ".ref($data), $data)
         if !$skip_trace and $self->trace;
+
     return $data;
 }
 # public aliases used by subclasses
@@ -103,6 +113,10 @@
 # and the tace level passed in
 sub _dump {
     my ($self, $label, $data) = @_;
+
+    # don't dump the binary
+    local $data->{meta}{frozen} if $data->{meta} && $data->{meta}{frozen};
+
     if ($self->trace >= 2) {
         require Data::Dumper;
         local $Data::Dumper::Indent    = 1;

Added: dbi/trunk/lib/DBI/Util/Cache.pm
==============================================================================
--- (empty file)
+++ dbi/trunk/lib/DBI/Util/Cache.pm     Fri Sep 28 09:11:07 2007
@@ -0,0 +1,113 @@
+package DBI::Util::Cache;
+
+#   $Id$
+#
+#   Copyright (c) 2007, Tim Bunce, Ireland
+#
+#   You may distribute under the terms of either the GNU General Public
+#   License or the Artistic License, as specified in the Perl README file.
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+DBI::Util::Cache - a fast but very minimal subset of Cache::Memory
+
+=head1 DESCRIPTION
+
+Like Cache::Memory (part of the Cache distribution) but doesn't support any 
fancy features.
+
+This module aims to be a very fast compatible strict sub-set for simple cases.
+
+=head1 METHODS WITH CHANGES
+
+=head2 new
+
+All options except C<namespace> are ignored.
+
+=head2 set
+
+Doesn't support expiry.
+
+=head2 purge
+
+Same as clear() - deletes everything in the namespace.
+
+=head1 METHODS WITHOUT CHANGES
+
+=over
+
+=item clear
+
+=item count
+
+=item exists
+
+=item remove
+
+=back
+
+=head1 UNSUPPORTED METHODS
+
+If it's not listed above, it's not supported.
+
+=cut
+
+our $VERSION = sprintf("0.%06d", q$Revision$ =~ /(\d+)/o);
+
+my %cache;
+
+sub new {
+    my ($class, %options ) = @_;
+    $options{namespace} ||= 'Default';
+    my $self =  bless {
+        #_cache => \%cache, # canbe handy for debugging/dumping
+        %options,
+    } => $class;
+    $self->clear;
+    return $self;
+}
+
+sub set {
+    my ($self, $key, $value) = @_;
+    $cache{ $self->{namespace} }->{$key} = $value;
+}
+
+sub get {
+    my ($self, $key) = @_;
+    return $cache{ $self->{namespace} }->{$key};
+}
+
+sub exists {
+    my ($self, $key) = @_;
+    return exists $cache{ $self->{namespace} }->{$key};
+}
+
+sub remove {
+    my ($self, $key) = @_;
+    return delete $cache{ $self->{namespace} }->{$key};
+}
+
+sub purge {
+    return shift->clear;
+}
+
+sub clear {
+    $cache{ shift->{namespace} } = {};
+}
+
+sub count {
+    return scalar keys %{ $cache{ shift->{namespace} } };
+}
+
+sub size {
+    my $c = $cache{ shift->{namespace} };
+    my $size = 0;
+    while ( my ($k,$v) = each %$c ) {
+        $size += length($k) + length($v);
+    }
+    return $size;
+}
+
+1;

Added: dbi/trunk/t/87gofer_cache.t
==============================================================================
--- (empty file)
+++ dbi/trunk/t/87gofer_cache.t Fri Sep 28 09:11:07 2007
@@ -0,0 +1,70 @@
+#!perl -w                                         # -*- perl -*-
+# vim:sw=4:ts=8
+$|=1;
+
+use strict;
+use warnings;
+
+use DBI;
+use Data::Dumper;
+use Test::More;
+use DBI::Util::Cache;
+
+plan 'no_plan';
+
+my @cache_classes = qw(DBI::Util::Cache);
+push @cache_classes, "Cache::Memory" if eval { require Cache::Memory };
+
+for my $cache_class (@cache_classes) {
+    my $cache_obj = $cache_class->new();
+    run_tests($cache_obj);
+}
+
+sub run_tests {
+    my $cache_obj = shift;
+
+    my $tmp;
+    my $dsn = "dbi:Gofer:transport=null;policy=classic;dsn=dbi:ExampleP:";
+    print " using $cache_obj for $dsn\n";
+
+    is $cache_obj->count, 0, 'cache should be empty to start';
+
+    my $dbh = DBI->connect($dsn, undef, undef, {
+        go_cache => $cache_obj,
+        RaiseError => 1, PrintError => 0, ShowErrorStatement => 1,
+    } );
+    ok my $go_transport = $dbh->{go_transport};
+
+    # setup
+    $cache_obj->clear;
+    is $cache_obj->count, 0, 'cache should be empty after clear';
+
+    $go_transport->transmit_count(0);
+    is $go_transport->transmit_count, 0, 'transmit_count should be 0';
+
+    $go_transport->cache_hit(0);
+    $go_transport->cache_miss(0);
+    $go_transport->cache_store(0);
+
+    # request 1
+    ok my $rows1 = $dbh->selectall_arrayref("select name from ?", {}, ".");
+    cmp_ok $cache_obj->count, '>', 0, 'cache should not be empty after select';
+
+    is $go_transport->cache_hit, 0;
+    is $go_transport->cache_miss, 1;
+    is $go_transport->cache_store, 1;
+
+    is $go_transport->transmit_count, 1, 'should make 1 round trip';
+    $go_transport->transmit_count(0);
+    is $go_transport->transmit_count, 0, 'transmit_count should be 0';
+
+    # request 2
+    ok my $rows2 = $dbh->selectall_arrayref("select name from ?", {}, ".");
+    is_deeply $rows2, $rows1;
+    is $go_transport->transmit_count, 0, 'should make 1 round trip';
+
+    is $go_transport->cache_hit, 1;
+    is $go_transport->cache_miss, 1;
+    is $go_transport->cache_store, 1;
+
+}

Reply via email to