Author: timbo
Date: Tue Oct 16 05:36:56 2007
New Revision: 10086

Added:
   dbi/trunk/lib/DBI/Util/CacheMemory.pm
      - copied, changed from r10085, /dbi/trunk/lib/DBI/Util/Cache.pm
Removed:
   dbi/trunk/lib/DBI/Util/Cache.pm
Modified:
   dbi/trunk/Changes
   dbi/trunk/MANIFEST
   dbi/trunk/lib/DBD/Gofer.pm
   dbi/trunk/lib/DBD/Gofer/Transport/Base.pm
   dbi/trunk/lib/DBD/Proxy.pm
   dbi/trunk/lib/DBI/Gofer/Request.pm
   dbi/trunk/t/87gofer_cache.t

Log:
Add client-side caching to DBD::Gofer.


Modified: dbi/trunk/Changes
==============================================================================
--- dbi/trunk/Changes   (original)
+++ dbi/trunk/Changes   Tue Oct 16 05:36:56 2007
@@ -48,9 +48,12 @@
     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
+  Added client-side caching to DBD::Gofer. Can use any cache with
+    get($k)/set($k,$v) methods, including all the Cache and Cache::Cache
+    distribution modules plus Cache::Memcached, Cache::FastMmap etc.
+    Works for all transports. Overridable per handle.
+
+  Added DBI::Util::CacheMemory 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

Modified: dbi/trunk/MANIFEST
==============================================================================
--- dbi/trunk/MANIFEST  (original)
+++ dbi/trunk/MANIFEST  Tue Oct 16 05:36:56 2007
@@ -63,7 +63,7 @@
 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 very�cut-down version of 
Class::Accessor::Fast
-lib/DBI/Util/Cache.pm           A very cut-down version of Cache::Memory
+lib/DBI/Util/CacheMemory.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

Modified: dbi/trunk/lib/DBD/Gofer.pm
==============================================================================
--- dbi/trunk/lib/DBD/Gofer.pm  (original)
+++ dbi/trunk/lib/DBD/Gofer.pm  Tue Oct 16 05:36:56 2007
@@ -55,9 +55,12 @@
         DBI->setup_driver('DBD::Gofer');
 
         unless ($methods_already_installed++) {
-            DBD::Gofer::db->install_method('go_dbh_method', { O=> 0x0004 }); # 
IMA_KEEP_ERR
-            DBD::Gofer::st->install_method('go_sth_method', { O=> 0x0004 }); # 
IMA_KEEP_ERR
-            DBD::Gofer::st->install_method('go_clone_sth',  { O=> 0x0004 }); # 
IMA_KEEP_ERR
+            my $opts = { O=> 0x0004 }; # IMA_KEEP_ERR
+            DBD::Gofer::db->install_method('go_dbh_method', $opts);
+            DBD::Gofer::st->install_method('go_sth_method', $opts);
+            DBD::Gofer::st->install_method('go_clone_sth',  $opts);
+            DBD::Gofer::db->install_method('go_cache',      $opts);
+            DBD::Gofer::st->install_method('go_cache',      $opts);
         }
 
         my($class, $attr) = @_;
@@ -77,6 +80,16 @@
     }
 
 
+    sub go_cache {
+        my $h = shift;
+        $h->{go_cache} = shift if @_;
+        # return handle's override go_cache, if it has one
+        return $h->{go_cache} if defined $h->{go_cache};
+        # or else the transports default go_cache
+        return $h->{go_transport}->{go_cache};
+    }
+
+
     sub set_err_from_response { # set error/warn/info and propagate warnings
         my ($h, $response) = @_;
         if (my $warnings = $response->warnings) {
@@ -168,10 +181,15 @@
         my $go_policy = $go_attr{go_policy};
 
         if ($go_attr{go_cache} and not ref $go_attr{go_cache}) { # if not a 
cache object already
-            $go_attr{go_cache} = eval { require DBI::Util::Cache; 
DBI::Util::Cache->new() };
+            my $cache_class = $go_attr{go_cache};
+            $cache_class = "DBI::Util::CacheMemory" if $cache_class eq '1';
+            _load_class($cache_class)
+                or return $drh->set_err($DBI::stderr, "Can't load $cache_class 
$@");
+            $go_attr{go_cache} = eval { $cache_class->new() }
+                or $drh->set_err(0, "Can't instanciate $cache_class: $@"); # 
warning
         }
 
-        # but delete any other attributes that don't appy to transport
+        # delete any other attributes that don't apply to transport
         my $go_connect_method = delete $go_attr{go_connect_method};
 
         my $transport_class = delete $go_attr{go_transport}
@@ -270,6 +288,9 @@
         my $transport = $dbh->{go_transport}
             or return $dbh->set_err($DBI::stderr, "Not connected (no 
transport)");
 
+        local $transport->{go_cache} = $dbh->{go_cache}
+            if defined $dbh->{go_cache};
+
         my ($response, $retransmit_sub) = 
$transport->transmit_request($request);
         $response ||= $transport->receive_response($request, $retransmit_sub);
         $dbh->{go_response} = $response
@@ -336,7 +357,7 @@
             my $cache;
             my $cache_key;
             if (my $cache_it = $dbh->{go_policy}->$policy_name(undef, $dbh, 
@_)) {
-                $cache = $dbh->{go_cache} ||= {};
+                $cache = $dbh->{go_meta_cache} ||= {}; # keep separate from 
go_cache
                 $cache_key = sprintf "%s_wa%d(%s)", $policy_name, wantarray||0,
                     join(",\t", map { # XXX basic but sufficient for now
                          !ref($_)            ? DBI::neat($_,1e6)
@@ -505,6 +526,7 @@
                       || $dbh->{go_prepare_method}
                       || $policy->prepare_method($dbh, $statement, $attr)
                       || 'prepare'; # e.g. for code not using placeholders
+        my $go_cache = delete $attr->{go_cache};
         # set to undef if there are no attributes left for the actual prepare 
call
         $attr = undef if $attr and not %$attr;
 
@@ -516,6 +538,7 @@
             go_transport => $dbh->{go_transport},
             go_policy => $policy,
             go_last_insert_id_args => $lii_args,
+            go_cache => $go_cache,
         });
         $sth->STORE(Active => 0);
 
@@ -536,6 +559,7 @@
         }, $if_active);
     }
 
+    *go_cache = \&DBD::Gofer::go_cache;
 }
 
 
@@ -588,6 +612,9 @@
         my $transport = $sth->{go_transport}
             or return $sth->set_err($DBI::stderr, "Not connected (no 
transport)");
 
+        local $transport->{go_cache} = $sth->{go_cache}
+            if defined $sth->{go_cache};
+
         my ($response, $retransmit_sub) = 
$transport->transmit_request($request);
         $response ||= $transport->receive_response($request, $retransmit_sub);
         $sth->{go_response} = $response
@@ -765,6 +792,7 @@
         return $sth->go_sth_method($attr);
     }
 
+    *go_cache = \&DBD::Gofer::go_cache;
 }
 
 1;
@@ -853,7 +881,8 @@
 
 =head3 Caching
 
-Not yet implemented, but the single request-response architecture lends itself 
to caching.
+Client-side caching is as simple as adding "C<cache=1>" to the DSN.
+This feature alone can be worth using DBD::Gofer for.
 
 =head3 Fewer Network Round-trips
 
@@ -1044,7 +1073,7 @@
 
 =head3 http
 
-See the GoferTransport-http distribution on CPAN.
+See the GoferTransport-http distribution on CPAN: 
http://search.cpan.org/dist/GoferTransport-http/
 
 =head3 Gearman
 
@@ -1059,7 +1088,7 @@
 
 Other attributes can be specified in the DSN to configure DBD::Gofer and/or the
 Gofer transport module being used. The main attributes after C<transport>, are
-C<url> and C<policy>. These are described below.
+C<url> and C<policy>. These and other attributes are described below.
 
 =head2 Using DBI_AUTOPROXY
 
@@ -1072,8 +1101,92 @@
 
     export DBI_AUTOPROXY="dbi:Gofer:transport=stream;url=ssh:[EMAIL PROTECTED]"
 
+=head2 Connection Attributes
+
+These attributes can be specified in the DSN. They can also be passed in the
+\%attr parameter of the DBI connect method by adding a "C<go_>" prefix to the 
name.
+
+=head3 transport
+
+Specifies the Gofer transport class to use. Required. See L</TRANSPORTS> above.
+
+If the value does not include C<::> then "C<DBD::Gofer::Transport::>" is 
prefixed.
+
+The transport object can be accessed via $h->{go_transport}.
+
+=head3 dsn
+
+Specifies the DSN for the remote side to connect to. Required, and must be 
last.
+
+=head3 url
+
+Used to tell the transport where to connect to. The exact form of the value 
depends on the transport used.
+
+=head3 policy
+
+Specifies the policy to use. See L</CONFIGURING BEHAVIOUR POLICY>.
+
+If the value does not include C<::> then "C<DBD::Gofer::Policy>" is prefixed.
+
+The policy object can be accessed via $h->{go_policy}.
+
+=head3 timeout
+
+Specifies a timeout, in seconds, to use when waiting for responses from the 
server side.
+
+=head3 retry_limit
+
+Specifies the number of times a failed request will be retried. Default is 0.
+
+=head3 retry_hook
+
+Specifies a code reference to be called to decide if a failed request should 
be retried.
+The code reference is called like this:
+
+  $transport = $h->{go_transport};
+  $retry = $transport->go_retry_hook->($request, $response, $transport);
+
+If it returns true then the request will be retried, upto the C<retry_limit>.
+If it returns a false but defined value then the request will not be retried.
+If it returns undef then the default behaviour will be used, as if 
C<retry_hook>
+had not been specified.
+
+The default behaviour is to retry requests where $request->is_idempotent is 
true,
+or the error message matches C</induced by DBI_GOFER_RANDOM/>.
+    
+=head3 cache
+
+Specifies that client-side caching should be performed.  The value is the name
+of a cache class to use.
+
+Any class implementing get($key) and set($key, $value) methods can be used.
+That includes a great many powerful caching classes on CPAN, including the
+Cache and Cache::Cache distributions.
+
+You can use "C<cache=1>" is a shortcut for "C<cache=DBI::Util::CacheMemory>".
+See L<DBI::Util::CacheMemory> for a description of this simple fast default 
cache.
+
+The cache object can be accessed via $h->go_cache. For example:
+
+    $dbh->go_cache->clear; # free up memory being used by the cache
+
+The cache keys are the frozen (serialized) requests, and the values are the
+frozen responses.
+
+The default behaviour is to only use the cache for requests where
+$request->is_idempotent is true (i.e., the dbh has the ReadOnly attribute set
+or the SQL statement is obviously a SELECT without a FOR UPDATE clause.)
+
+For even more control you can use the C<go_cache> attribute to pass in an
+instanciated cache object. Individual methods, including prepare(), can also
+specify alternative caches via the C<go_cache> attribute. For example, to
+specify no caching for a particular query, you could use
+
+    $sth = $dbh->prepare( $sql, { go_cache => 0 } );
+
+This can be used to implement different caching policies for different 
statements.
 
-=head1 CONFIGURING VIA POLICY
+=head1 CONFIGURING BEHAVIOUR POLICY
 
 DBD::Gofer supports a 'policy' mechanism that allows you to fine-tune the 
number of round-trips to the Gofer server.
 The policies are grouped into classes (which may be subclassed) and referenced 
by the name of the class.

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   Tue Oct 16 05:36:56 2007
@@ -54,19 +54,26 @@
     my ($self, $request) = @_;
     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;
+    my ($go_cache, $request_cache_key);
+    if ($go_cache = $self->{go_cache}) {
+        $request_cache_key
+            = $request->{meta}{request_cache_key}
+            = $self->get_cache_key_for_request($request);
+        if ($request_cache_key) {
+            my $frozen_response = eval { $go_cache->get($request_cache_key) };
+            if ($frozen_response) {
+                $response = $self->thaw_response($frozen_response);
+                if (my $trace = $self->trace) {
+                    $self->_dump("cached response found for ".ref($request), 
$request);
+                    $self->_dump("cached response is ".ref($response), 
$response);
+                    $self->trace_msg("transmit_request is returing a response 
from cache\n");
+                }
+                ++$self->{cache_hit};
+                return $response;
+            }
+            warn $@ if $@;
+            ++$self->{cache_miss};
         }
-        ++$self->{cache_miss} if $request_key;
     }
 
     my $to = $self->go_timeout;
@@ -95,6 +102,12 @@
 
     $response = $self->_transmit_request_with_retries($request, $transmit_sub);
 
+    if ($response) {
+        my $frozen_response = delete $response->{meta}{frozen};
+        $self->_store_response_in_cache($frozen_response, $request_cache_key)
+            if $go_cache;
+    }
+
     $self->trace_msg("transmit_request is returing a response itself\n") if 
$response;
 
     return $response unless wantarray;
@@ -143,19 +156,10 @@
         }
     } while ( $self->response_needs_retransmit($request, $response) );
 
-    my $frozen_response = delete $response->{meta}{frozen};
-
-    if (my $go_cache = $self->{go_cache}) {
-
-       # new() ensures that enabling go_cache also enabled keep_meta_frozen
-        warn "No meta frozen in request" if !$frozen_response;
-
-        my $request_key = $self->get_cache_key_for_request($request);
-        if ($frozen_response && $request_key) {
-            $self->trace_msg("receive_response added response to cache\n");
-            $go_cache->set($request_key, $frozen_response);
-            ++$self->{cache_store};
-        }
+    if ($response) {
+        my $frozen_response = delete $response->{meta}{frozen};
+        $self->_store_response_in_cache($frozen_response, 
$request->{meta}{request_cache_key})
+            if $self->{go_cache};
     }
 
     return $response;
@@ -218,6 +222,7 @@
 
 
 # return undef if we don't want to cache this request
+# subclasses may use more specialized rules
 sub get_cache_key_for_request {
     my ($self, $request) = @_;
 
@@ -234,6 +239,23 @@
 }
 
 
+sub _store_response_in_cache {
+    my ($self, $frozen_response, $request_cache_key) = @_;
+    my $go_cache = $self->{go_cache}
+        or return;
+
+    # new() ensures that enabling go_cache also enables keep_meta_frozen
+    warn "No meta frozen in response" if !$frozen_response;
+    warn "No request_cache_key" if !$request_cache_key;
+
+    if ($frozen_response && $request_cache_key) {
+        $self->trace_msg("receive_response added response to cache\n");
+        eval { $go_cache->set($request_cache_key, $frozen_response) };
+        warn $@ if $@;
+        ++$self->{cache_store};
+    }
+}
+
 1;
 
 =head1 NAME

Modified: dbi/trunk/lib/DBD/Proxy.pm
==============================================================================
--- dbi/trunk/lib/DBD/Proxy.pm  (original)
+++ dbi/trunk/lib/DBD/Proxy.pm  Tue Oct 16 05:36:56 2007
@@ -741,7 +741,7 @@
 =head1 DESCRIPTION
 
 DBD::Proxy is a Perl module for connecting to a database via a remote
-DBI driver.
+DBI driver. See L<DBD::Gofer> for an alternative with different trade-offs.
 
 This is of course not needed for DBI drivers which already
 support connecting to a remote database, but there are engines which

Modified: dbi/trunk/lib/DBI/Gofer/Request.pm
==============================================================================
--- dbi/trunk/lib/DBI/Gofer/Request.pm  (original)
+++ dbi/trunk/lib/DBI/Gofer/Request.pm  Tue Oct 16 05:36:56 2007
@@ -85,12 +85,14 @@
         return 1 if $flags & (GOf_REQUEST_IDEMPOTENT|GOf_REQUEST_READONLY);
     }
 
-    # else check if all statements are select statement
+    # else check if all statements are SELECT statement that don't include FOR 
UPDATE
     my @statements = $self->statements;
     # XXX this is very minimal for now, doesn't even allow comments before the 
select
     # (and can't ever work for "exec stored_procedure_name" kinds of 
statements)
     # XXX it also doesn't deal with multiple statements: prepare("select foo; 
update bar")
-    return 1 if @statements == grep { m/^ \s* SELECT \b/xmsi } @statements;
+    return 1 if @statements == grep {
+                m/^ \s* SELECT \b /xmsi && !m/ \b FOR \s+ UPDATE \b /xmsi
+             } @statements;
 
     return 0;
 }

Copied: dbi/trunk/lib/DBI/Util/CacheMemory.pm (from r10085, 
/dbi/trunk/lib/DBI/Util/Cache.pm)
==============================================================================
--- /dbi/trunk/lib/DBI/Util/Cache.pm    (original)
+++ dbi/trunk/lib/DBI/Util/CacheMemory.pm       Tue Oct 16 05:36:56 2007
@@ -1,4 +1,4 @@
-package DBI::Util::Cache;
+package DBI::Util::CacheMemory;
 
 #   $Id$
 #
@@ -12,7 +12,7 @@
 
 =head1 NAME
 
-DBI::Util::Cache - a very fast but very minimal subset of Cache::Memory
+DBI::Util::CacheMemory - a very fast but very minimal subset of Cache::Memory
 
 =head1 DESCRIPTION
 

Modified: dbi/trunk/t/87gofer_cache.t
==============================================================================
--- dbi/trunk/t/87gofer_cache.t (original)
+++ dbi/trunk/t/87gofer_cache.t Tue Oct 16 05:36:56 2007
@@ -8,24 +8,29 @@
 use DBI;
 use Data::Dumper;
 use Test::More;
-use DBI::Util::Cache;
+use DBI::Util::CacheMemory;
+
+plan skip_all => "Gofer DBI_AUTOPROXY" if $ENV{DBI_AUTOPROXY} =~ /^dbi:Gofer/i;
 
 plan 'no_plan';
 
-my @cache_classes = qw(DBI::Util::Cache);
+
+my $dsn = "dbi:Gofer:transport=null;policy=classic;dsn=dbi:ExampleP:";
+
+my @cache_classes = qw(DBI::Util::CacheMemory);
 push @cache_classes, "Cache::Memory" if eval { require Cache::Memory };
-push @cache_classes, "1";
+push @cache_classes, "1"; # test alias for DBI::Util::CacheMemory
 
 for my $cache_class (@cache_classes) {
     my $cache_obj = ($cache_class eq "1") ? $cache_class : $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";
 
     my $dbh = DBI->connect($dsn, undef, undef, {
@@ -67,7 +72,37 @@
     is $go_transport->cache_hit, $expected;
     is $go_transport->cache_miss, $expected;
     is $go_transport->cache_store, $expected;
-
 }
 
+
+print "test per-sth go_cache\n";
+
+my $dbh = DBI->connect($dsn, undef, undef, {
+    go_cache => 1,
+    RaiseError => 1, PrintError => 0, ShowErrorStatement => 1,
+} );
+ok my $go_transport = $dbh->{go_transport};
+ok my $dbh_cache = $go_transport->go_cache;
+$dbh_cache->clear; # discard ping from connect
+
+my $cache2 = DBI::Util::CacheMemory->new( namespace => "foo2" );
+ok $cache2;
+ok $cache2 != $dbh_cache;
+
+my $sth1 = $dbh->prepare("select name from ?");
+is $sth1->go_cache, $dbh_cache;
+is $dbh_cache->size, 0;
+ok $dbh->selectall_arrayref($sth1, undef, ".");
+ok $dbh_cache->size;
+
+my $sth2 = $dbh->prepare("select * from ?", { go_cache => $cache2 });
+is $sth2->go_cache, $cache2;
+is $cache2->size, 0;
+ok $dbh->selectall_arrayref($sth2, undef, ".");
+ok $cache2->size;
+
+cmp_ok $cache2->size, '>', $dbh_cache->size;
+
+
+
 1;

Reply via email to