1 2package Memoize::Expire; 3# require 5.00556; 4use Carp; 5$DEBUG = 0; 6$VERSION = '1.00'; 7 8# This package will implement expiration by prepending a fixed-length header 9# to the font of the cached data. The format of the header will be: 10# (4-byte number of last-access-time) (For LRU when I implement it) 11# (4-byte expiration time: unsigned seconds-since-unix-epoch) 12# (2-byte number-of-uses-before-expire) 13 14sub _header_fmt () { "N N n" } 15sub _header_size () { length(_header_fmt) } 16 17# Usage: memoize func 18# TIE => [Memoize::Expire, LIFETIME => sec, NUM_USES => n, 19# TIE => [...] ] 20 21BEGIN { 22 eval {require Time::HiRes}; 23 unless ($@) { 24 Time::HiRes->import('time'); 25 } 26} 27 28sub TIEHASH { 29 my ($package, %args) = @_; 30 my %cache; 31 if ($args{TIE}) { 32 my ($module, @opts) = @{$args{TIE}}; 33 my $modulefile = $module . '.pm'; 34 $modulefile =~ s{::}{/}g; 35 eval { require $modulefile }; 36 if ($@) { 37 croak "Memoize::Expire: Couldn't load hash tie module `$module': $@; aborting"; 38 } 39 my $rc = (tie %cache => $module, @opts); 40 unless ($rc) { 41 croak "Memoize::Expire: Couldn't tie hash to `$module': $@; aborting"; 42 } 43 } 44 $args{LIFETIME} ||= 0; 45 $args{NUM_USES} ||= 0; 46 $args{C} = \%cache; 47 bless \%args => $package; 48} 49 50sub STORE { 51 $DEBUG and print STDERR " >> Store $_[1] $_[2]\n"; 52 my ($self, $key, $value) = @_; 53 my $expire_time = $self->{LIFETIME} > 0 ? $self->{LIFETIME} + time : 0; 54 # The call that results in a value to store into the cache is the 55 # first of the NUM_USES allowed calls. 56 my $header = _make_header(time, $expire_time, $self->{NUM_USES}-1); 57 $self->{C}{$key} = $header . $value; 58 $value; 59} 60 61sub FETCH { 62 $DEBUG and print STDERR " >> Fetch cached value for $_[1]\n"; 63 my ($data, $last_access, $expire_time, $num_uses_left) = _get_item($_[0]{C}{$_[1]}); 64 $DEBUG and print STDERR " >> (ttl: ", ($expire_time-time()), ", nuses: $num_uses_left)\n"; 65 $num_uses_left--; 66 $last_access = time; 67 _set_header(@_, $data, $last_access, $expire_time, $num_uses_left); 68 $data; 69} 70 71sub EXISTS { 72 $DEBUG and print STDERR " >> Exists $_[1]\n"; 73 unless (exists $_[0]{C}{$_[1]}) { 74 $DEBUG and print STDERR " Not in underlying hash at all.\n"; 75 return 0; 76 } 77 my $item = $_[0]{C}{$_[1]}; 78 my ($last_access, $expire_time, $num_uses_left) = _get_header($item); 79 my $ttl = $expire_time - time; 80 if ($DEBUG) { 81 $_[0]{LIFETIME} and print STDERR " Time to live for this item: $ttl\n"; 82 $_[0]{NUM_USES} and print STDERR " Uses remaining: $num_uses_left\n"; 83 } 84 if ( (! $_[0]{LIFETIME} || $expire_time > time) 85 && (! $_[0]{NUM_USES} || $num_uses_left > 0 )) { 86 $DEBUG and print STDERR " (Still good)\n"; 87 return 1; 88 } else { 89 $DEBUG and print STDERR " (Expired)\n"; 90 return 0; 91 } 92} 93 94# Arguments: last access time, expire time, number of uses remaining 95sub _make_header { 96 pack "N N n", @_; 97} 98 99sub _strip_header { 100 substr($_[0], 10); 101} 102 103# Arguments: last access time, expire time, number of uses remaining 104sub _set_header { 105 my ($self, $key, $data, @header) = @_; 106 $self->{C}{$key} = _make_header(@header) . $data; 107} 108 109sub _get_item { 110 my $data = substr($_[0], 10); 111 my @header = unpack "N N n", substr($_[0], 0, 10); 112# print STDERR " >> _get_item: $data => $data @header\n"; 113 ($data, @header); 114} 115 116# Return last access time, expire time, number of uses remaining 117sub _get_header { 118 unpack "N N n", substr($_[0], 0, 10); 119} 120 1211; 122 123=head1 NAME 124 125Memoize::Expire - Plug-in module for automatic expiration of memoized values 126 127=head1 SYNOPSIS 128 129 use Memoize; 130 use Memoize::Expire; 131 tie my %cache => 'Memoize::Expire', 132 LIFETIME => $lifetime, # In seconds 133 NUM_USES => $n_uses; 134 135 memoize 'function', SCALAR_CACHE => [HASH => \%cache ]; 136 137=head1 DESCRIPTION 138 139Memoize::Expire is a plug-in module for Memoize. It allows the cached 140values for memoized functions to expire automatically. This manual 141assumes you are already familiar with the Memoize module. If not, you 142should study that manual carefully first, paying particular attention 143to the HASH feature. 144 145Memoize::Expire is a layer of software that you can insert in between 146Memoize itself and whatever underlying package implements the cache. 147The layer presents a hash variable whose values expire whenever they 148get too old, have been used too often, or both. You tell C<Memoize> to 149use this forgetful hash as its cache instead of the default, which is 150an ordinary hash. 151 152To specify a real-time timeout, supply the C<LIFETIME> option with a 153numeric value. Cached data will expire after this many seconds, and 154will be looked up afresh when it expires. When a data item is looked 155up afresh, its lifetime is reset. 156 157If you specify C<NUM_USES> with an argument of I<n>, then each cached 158data item will be discarded and looked up afresh after the I<n>th time 159you access it. When a data item is looked up afresh, its number of 160uses is reset. 161 162If you specify both arguments, data will be discarded from the cache 163when either expiration condition holds. 164 165Memoize::Expire uses a real hash internally to store the cached data. 166You can use the C<HASH> option to Memoize::Expire to supply a tied 167hash in place of the ordinary hash that Memoize::Expire will normally 168use. You can use this feature to add Memoize::Expire as a layer in 169between a persistent disk hash and Memoize. If you do this, you get a 170persistent disk cache whose entries expire automatically. For 171example: 172 173 # Memoize 174 # | 175 # Memoize::Expire enforces data expiration policy 176 # | 177 # DB_File implements persistence of data in a disk file 178 # | 179 # Disk file 180 181 use Memoize; 182 use Memoize::Expire; 183 use DB_File; 184 185 # Set up persistence 186 tie my %disk_cache => 'DB_File', $filename, O_CREAT|O_RDWR, 0666]; 187 188 # Set up expiration policy, supplying persistent hash as a target 189 tie my %cache => 'Memoize::Expire', 190 LIFETIME => $lifetime, # In seconds 191 NUM_USES => $n_uses, 192 HASH => \%disk_cache; 193 194 # Set up memoization, supplying expiring persistent hash for cache 195 memoize 'function', SCALAR_CACHE => [ HASH => \%cache ]; 196 197=head1 INTERFACE 198 199There is nothing special about Memoize::Expire. It is just an 200example. If you don't like the policy that it implements, you are 201free to write your own expiration policy module that implements 202whatever policy you desire. Here is how to do that. Let us suppose 203that your module will be named MyExpirePolicy. 204 205Short summary: You need to create a package that defines four methods: 206 207=over 4 208 209=item 210TIEHASH 211 212Construct and return cache object. 213 214=item 215EXISTS 216 217Given a function argument, is the corresponding function value in the 218cache, and if so, is it fresh enough to use? 219 220=item 221FETCH 222 223Given a function argument, look up the corresponding function value in 224the cache and return it. 225 226=item 227STORE 228 229Given a function argument and the corresponding function value, store 230them into the cache. 231 232=item 233CLEAR 234 235(Optional.) Flush the cache completely. 236 237=back 238 239The user who wants the memoization cache to be expired according to 240your policy will say so by writing 241 242 tie my %cache => 'MyExpirePolicy', args...; 243 memoize 'function', SCALAR_CACHE => [HASH => \%cache]; 244 245This will invoke C<< MyExpirePolicy->TIEHASH(args) >>. 246MyExpirePolicy::TIEHASH should do whatever is appropriate to set up 247the cache, and it should return the cache object to the caller. 248 249For example, MyExpirePolicy::TIEHASH might create an object that 250contains a regular Perl hash (which it will to store the cached 251values) and some extra information about the arguments and how old the 252data is and things like that. Let us call this object `C'. 253 254When Memoize needs to check to see if an entry is in the cache 255already, it will invoke C<< C->EXISTS(key) >>. C<key> is the normalized 256function argument. MyExpirePolicy::EXISTS should return 0 if the key 257is not in the cache, or if it has expired, and 1 if an unexpired value 258is in the cache. It should I<not> return C<undef>, because there is a 259bug in some versions of Perl that will cause a spurious FETCH if the 260EXISTS method returns C<undef>. 261 262If your EXISTS function returns true, Memoize will try to fetch the 263cached value by invoking C<< C->FETCH(key) >>. MyExpirePolicy::FETCH should 264return the cached value. Otherwise, Memoize will call the memoized 265function to compute the appropriate value, and will store it into the 266cache by calling C<< C->STORE(key, value) >>. 267 268Here is a very brief example of a policy module that expires each 269cache item after ten seconds. 270 271 package Memoize::TenSecondExpire; 272 273 sub TIEHASH { 274 my ($package, %args) = @_; 275 my $cache = $args{HASH} || {}; 276 bless $cache => $package; 277 } 278 279 sub EXISTS { 280 my ($cache, $key) = @_; 281 if (exists $cache->{$key} && 282 $cache->{$key}{EXPIRE_TIME} > time) { 283 return 1 284 } else { 285 return 0; # Do NOT return `undef' here. 286 } 287 } 288 289 sub FETCH { 290 my ($cache, $key) = @_; 291 return $cache->{$key}{VALUE}; 292 } 293 294 sub STORE { 295 my ($cache, $key, $newvalue) = @_; 296 $cache->{$key}{VALUE} = $newvalue; 297 $cache->{$key}{EXPIRE_TIME} = time + 10; 298 } 299 300To use this expiration policy, the user would say 301 302 use Memoize; 303 tie my %cache10sec => 'Memoize::TenSecondExpire'; 304 memoize 'function', SCALAR_CACHE => [HASH => \%cache10sec]; 305 306Memoize would then call C<function> whenever a cached value was 307entirely absent or was older than ten seconds. 308 309You should always support a C<HASH> argument to C<TIEHASH> that ties 310the underlying cache so that the user can specify that the cache is 311also persistent or that it has some other interesting semantics. The 312example above demonstrates how to do this, as does C<Memoize::Expire>. 313 314=head1 ALTERNATIVES 315 316Brent Powers has a C<Memoize::ExpireLRU> module that was designed to 317work with Memoize and provides expiration of least-recently-used data. 318The cache is held at a fixed number of entries, and when new data 319comes in, the least-recently used data is expired. See 320L<http://search.cpan.org/search?mode=module&query=ExpireLRU>. 321 322Joshua Chamas's Tie::Cache module may be useful as an expiration 323manager. (If you try this, let me know how it works out.) 324 325If you develop any useful expiration managers that you think should be 326distributed with Memoize, please let me know. 327 328=head1 CAVEATS 329 330This module is experimental, and may contain bugs. Please report bugs 331to the address below. 332 333Number-of-uses is stored as a 16-bit unsigned integer, so can't exceed 33465535. 335 336Because of clock granularity, expiration times may occur up to one 337second sooner than you expect. For example, suppose you store a value 338with a lifetime of ten seconds, and you store it at 12:00:00.998 on a 339certain day. Memoize will look at the clock and see 12:00:00. Then 3409.01 seconds later, at 12:00:10.008 you try to read it back. Memoize 341will look at the clock and see 12:00:10 and conclude that the value 342has expired. This will probably not occur if you have 343C<Time::HiRes> installed. 344 345=head1 AUTHOR 346 347Mark-Jason Dominus (mjd-perl-memoize+@plover.com) 348 349Mike Cariaso provided valuable insight into the best way to solve this 350problem. 351 352=head1 SEE ALSO 353 354perl(1) 355 356The Memoize man page. 357 358http://www.plover.com/~mjd/perl/Memoize/ (for news and updates) 359 360I maintain a mailing list on which I occasionally announce new 361versions of Memoize. The list is for announcements only, not 362discussion. To join, send an empty message to 363mjd-perl-memoize-request@Plover.com. 364 365=cut 366