Skip Menu |

This queue is for tickets about the Memoize CPAN distribution.

Report information
The Basics
Id: 38344
Status: open
Priority: 0/
Queue: Memoize

People
Owner: Nobody in particular
Requestors: w.phillip.moore [...] gmail.com
Cc:
AdminCc:

Bug Information
Severity: Important
Broken in: 1.01
Fixed in: (no value)



Subject: Memoize::Expire stringifies the caches values, making it useless for functions that return objects
If you try to use Memoize::Expire to memoize a function or method which returns an object, then the first time anything is retrieved from the cache, you discover you have a stringified version of the object, not the actual blessed reference. This is because Memoize::Expire prepend the metadata for the lifetime and number of uses remaining to the front of the cached value, and act of concatenation stringifies the object. I've added a test case that exposes the bug, and then reworked the internals of Memoize::Expire to use a hash reference to track the metadata, and the actual data values, without accidentally stringifying them. This patch should allow Memoize::Expire to be used with functions that return arbitrary data, although I have only tested it for one case: a function that returns a single object, which was the use case of mine that exposed the bug. I have no incremented the version number in this release, and I'll leave that up to Mark Jason.
Subject: Memoize-1.01-no-stringify-values.patch
diff -rc Memoize-1.01-orig/Memoize/Expire.pm Memoize-1.01/Memoize/Expire.pm *** Memoize-1.01-orig/Memoize/Expire.pm 2002-03-29 13:51:27.000000000 -0500 --- Memoize-1.01/Memoize/Expire.pm 2008-08-08 16:41:42.000000000 -0400 *************** *** 1,22 **** package Memoize::Expire; # require 5.00556; use Carp; $DEBUG = 0; $VERSION = '1.00'; ! # This package will implement expiration by prepending a fixed-length header ! # to the font of the cached data. The format of the header will be: ! # (4-byte number of last-access-time) (For LRU when I implement it) ! # (4-byte expiration time: unsigned seconds-since-unix-epoch) ! # (2-byte number-of-uses-before-expire) ! ! sub _header_fmt () { "N N n" } ! sub _header_size () { length(_header_fmt) } ! ! # Usage: memoize func ! # TIE => [Memoize::Expire, LIFETIME => sec, NUM_USES => n, ! # TIE => [...] ] BEGIN { eval {require Time::HiRes}; --- 1,15 ---- package Memoize::Expire; # require 5.00556; + use strict; use Carp; + + use vars qw( $DEBUG $VERSION ); + $DEBUG = 0; $VERSION = '1.00'; ! use Data::Dumper; BEGIN { eval {require Time::HiRes}; *************** *** 48,88 **** } sub STORE { - $DEBUG and print STDERR " >> Store $_[1] $_[2]\n"; my ($self, $key, $value) = @_; my $expire_time = $self->{LIFETIME} > 0 ? $self->{LIFETIME} + time : 0; # The call that results in a value to store into the cache is the # first of the NUM_USES allowed calls. ! my $header = _make_header(time, $expire_time, $self->{NUM_USES}-1); ! $self->{C}{$key} = $header . $value; $value; } sub FETCH { ! $DEBUG and print STDERR " >> Fetch cached value for $_[1]\n"; ! my ($data, $last_access, $expire_time, $num_uses_left) = _get_item($_[0]{C}{$_[1]}); ! $DEBUG and print STDERR " >> (ttl: ", ($expire_time-time()), ", nuses: $num_uses_left)\n"; ! $num_uses_left--; ! $last_access = time; ! _set_header(@_, $data, $last_access, $expire_time, $num_uses_left); ! $data; } sub EXISTS { ! $DEBUG and print STDERR " >> Exists $_[1]\n"; ! unless (exists $_[0]{C}{$_[1]}) { $DEBUG and print STDERR " Not in underlying hash at all.\n"; return 0; } ! my $item = $_[0]{C}{$_[1]}; ! my ($last_access, $expire_time, $num_uses_left) = _get_header($item); ! my $ttl = $expire_time - time; if ($DEBUG) { ! $_[0]{LIFETIME} and print STDERR " Time to live for this item: $ttl\n"; ! $_[0]{NUM_USES} and print STDERR " Uses remaining: $num_uses_left\n"; } ! if ( (! $_[0]{LIFETIME} || $expire_time > time) ! && (! $_[0]{NUM_USES} || $num_uses_left > 0 )) { $DEBUG and print STDERR " (Still good)\n"; return 1; } else { --- 41,91 ---- } sub STORE { my ($self, $key, $value) = @_; + $DEBUG and print STDERR " >> Store $key => $value\n"; my $expire_time = $self->{LIFETIME} > 0 ? $self->{LIFETIME} + time : 0; # The call that results in a value to store into the cache is the # first of the NUM_USES allowed calls. ! $self->{C}{$key} = { ! expire_time => $expire_time, ! num_uses_left => $self->{NUM_USES}-1, ! value => $value, ! }; $value; } sub FETCH { ! my ($self, $key) = @_; ! my $data = $self->{C}{$key}; ! $DEBUG and print STDERR "Data for $key is:\n" . Data::Dumper->Dump([$data],['data']); ! my $value = $data->{value}; ! $DEBUG and print STDERR " >> Fetch cached value for $key => $value\n"; ! if ( $self->{LIFETIME} ) { ! my $ttl = $data->{expire_time} - time(); ! $DEBUG and print STDERR " >> (ttl: $ttl)\n"; ! } ! if ( $self->{NUM_USES} ) { ! $DEBUG and print STDERR " >> (nuses: $data->{num_uses})\n"; ! $data->{num_uses_left}--; ! } ! $value; } sub EXISTS { ! my ($self, $key) = @_; ! $DEBUG and print STDERR " >> Exists $key\n"; ! unless (exists $self->{C}{$key}) { $DEBUG and print STDERR " Not in underlying hash at all.\n"; return 0; } ! my $data = $self->{C}{$key}; ! my $ttl = $data->{expire_time} - time; if ($DEBUG) { ! $self->{LIFETIME} and print STDERR " Time to live for this item: $ttl\n"; ! $self->{NUM_USES} and print STDERR " Uses remaining: $data->{num_uses_left}\n"; } ! if ( (! $self->{LIFETIME} || $data->{expire_time} > time) ! && (! $self->{NUM_USES} || $data->{num_uses_left} > 0 )) { $DEBUG and print STDERR " (Still good)\n"; return 1; } else { *************** *** 91,123 **** } } - # Arguments: last access time, expire time, number of uses remaining - sub _make_header { - pack "N N n", @_; - } - - sub _strip_header { - substr($_[0], 10); - } - - # Arguments: last access time, expire time, number of uses remaining - sub _set_header { - my ($self, $key, $data, @header) = @_; - $self->{C}{$key} = _make_header(@header) . $data; - } - - sub _get_item { - my $data = substr($_[0], 10); - my @header = unpack "N N n", substr($_[0], 0, 10); - # print STDERR " >> _get_item: $data => $data @header\n"; - ($data, @header); - } - - # Return last access time, expire time, number of uses remaining - sub _get_header { - unpack "N N n", substr($_[0], 0, 10); - } - 1; =head1 NAME --- 94,99 ---- diff -rc Memoize-1.01-orig/t/expmod_t.t Memoize-1.01/t/expmod_t.t *** Memoize-1.01-orig/t/expmod_t.t 2002-07-12 13:57:12.000000000 -0400 --- Memoize-1.01/t/expmod_t.t 2008-08-08 16:45:15.000000000 -0400 *************** *** 24,30 **** # Perhaps nobody will notice if we don't say anything # print "# Warning: I'm testing the timed expiration policy.\n# This will take about thirty seconds.\n"; ! print "1..15\n"; $| = 1; # (1) --- 24,30 ---- # Perhaps nobody will notice if we don't say anything # print "# Warning: I'm testing the timed expiration policy.\n# This will take about thirty seconds.\n"; ! print "1..17\n"; $| = 1; # (1) *************** *** 134,136 **** --- 134,169 ---- } } + sub object { + return MyObject->new(time); + } + + tie my %object_cache => 'Memoize::Expire', LIFETIME => 15; + memoize 'object', + SCALAR_CACHE => [HASH => \%object_cache ], + LIST_CACHE => 'FAULT' + ; + + my $first = object(1); + ++$n; + if ( ref $first && $first->isa('MyObject') ) { + print "ok $n # looks like first is a " . (ref $first) . "\n"; + } else { + print "not ok $n # expected first to be MyObject, but it's '$first'\n"; + } + + my $second = object(1); + ++$n; + if ( ref $second && $second->isa('MyObject') ) { + print "ok $n # looks like second is a " . (ref $second) ."\n"; + } else { + print "not ok $n # expected second to be MyObject, but it's '$second'\n"; + } + + package MyObject; + + sub new { + my $class = shift; + my $time = shift; + return bless \$time, $class; + }
Subject: [rt.cpan.org #38344] Memoize: Memoize::Expire stringifies the caches values, making it useless for functions that return objects
Date: Thu, 8 Oct 2015 16:30:43 +0200
To: bug-Memoize [...] rt.cpan.org
From: Peter Valdemar Mørch <peter [...] morch.com>
Hi, I've updated the patch for 1.03 (tiny change, but there was a conflict on the version number). I'd like to point out that this patch as been available and not applied for 7 years. Perhaps the severity of the consequences isn't clear: The previous code did: $self->{C}{$key} = $header . $value; Because of that, if you have a sub that you Memoize with Memoize::Expire: sub foo { return { foo => "bar" } } Then Memoize::Expire will give you the datastructure { foo => "bar" } the first time (when it hasn't been through the cache yet), but the **string** "HASH(0x16d7e78)" the second time - which is useless. It can therefore only cache non-refs. I don't believe anybody can use Memoize::Expire for anything serious with this bug. I would therefore like to ask for this to be applied. Or rejected for some reason. Peter

Message body is not shown because sender requested not to inline it.

On 2008-08-08 17:45:34, WPMOORE wrote: Show quoted text
> If you try to use Memoize::Expire to memoize a function or method which > returns an object, then the first time anything is retrieved from the > cache, you discover you have a stringified version of the object, not > the actual blessed reference. > > This is because Memoize::Expire prepend the metadata for the lifetime > and number of uses remaining to the front of the cached value, and act > of concatenation stringifies the object. > > I've added a test case that exposes the bug, and then reworked the > internals of Memoize::Expire to use a hash reference to track the > metadata, and the actual data values, without accidentally stringifying > them. > > This patch should allow Memoize::Expire to be used with functions that > return arbitrary data, although I have only tested it for one case: a > function that returns a single object, which was the use case of mine > that exposed the bug. > > I have no incremented the version number in this release, and I'll leave > that up to Mark Jason.
There's a problem with the patch: the values in the C cache would became complex, so using an external flat database like DB_File (as proposed in the Memoize::Expire documentation) would not work anymore, even for non-complex data. The current situation without the patch looks like this: * non-complex data + in memory hash -> works * complex data + in memory hash -> does not work * non-complex data + DB_File -> works * complex data + DB_File -> does not work With the patch: * non-complex data + in memory hash -> works * complex data + in memory hash -> works (improvement) * non-complex data + DB_File -> does not work (regression) * complex data + DB_File -> does not work A possibility would be to use MLDBM instead of DB_File. In this case using both non-complex and complex data should probably work (untested). However, MLDBM is a non-core perl module, and people might prefer in some situations to stick to core modules only, so it would be good to keep the currently working combination on-complex data + DB_File. Maybe introduce another option?