File Coverage

blib/lib/Cache/Adaptive.pm
Criterion Covered Total %
statement 65 65 100.0
branch 26 32 81.2
condition 7 17 41.1
subroutine 8 8 100.0
pod 2 2 100.0
total 108 124 87.1


line stmt bran cond sub pod time code
1             #
2             # Copyright (c) 2007 Cybozu Labs, Inc. All rights reserved.
3             #
4              
5             package Cache::Adaptive;
6 2     2   11 use strict;
  2         3  
  2         70  
7 2     2   10 use warnings;
  2         5  
  2         76  
8 2     2   11 use base qw(Class::Accessor::Fast);
  2         4  
  2         2110  
9 2     2   7451 use List::Util qw(min max reduce);
  2         5  
  2         304  
10 2     2   6286 use Time::HiRes qw(gettimeofday tv_interval);
  2         10550  
  2         12  
11              
12             our $VERSION = '0.03';
13              
14             my %DEFAULTS = (
15             backend => undef,
16             check_interval => 0,
17             check_load => sub { int(shift->{process_time} * 2) - 1 },
18             expires_initial => 5,
19             expires_min => 1,
20             expires_max => 60,
21             increase_factor => 1.5,
22             decrease_factor => 0.8,
23             log => sub {},
24             );
25              
26             __PACKAGE__->mk_accessors($_) for (keys(%DEFAULTS), qw(purge_after));
27              
28             sub new {
29 1     1 1 3 my ($class, $opts) = @_;
30 1 50       16 my $self = bless {
31             %DEFAULTS,
32             $opts ? %$opts : (),
33             }, $class;
34 1 50       7 die "no backend\n" unless $self->backend;
35 1   33     24 $self->{purge_after} ||= $self->{expires_max} * 2;
36 1         3 $self;
37             }
38              
39             sub access {
40 37     37 1 181 my ($self, $opts) = @_;
41            
42 37 50       236 die "no key\n" unless $opts->{key};
43 37 50       201 die "no builder callback\n" unless $opts->{builder};
44            
45 37         222 my $at = gettimeofday;
46            
47             # obtain cache entry, return it if possible, or build a new entry
48 37         297 my $entry = $self->backend->get($opts->{key});
49 37   33     12572 my $purge_after = $opts->{purge_after} || $self->purge_after;
50 37 100       418 if ($entry) {
51 35 100 66     318 if ($entry->{value} && ! $opts->{force}) {
52 33         342 my $expires_at =
53             $entry->{expires_at} - rand() * $entry->{expires_in} * 0.2;
54 33 100 66     249 if ($entry->{_no_write} || $at < $expires_at) {
55             # printf(STDERR "Cache-Adaptive $$ %s no write is on\n", $entry->{build_at}) if $entry->{_no_write};
56 27         552 $self->log->({
57             %$opts,
58             type => q(hit),
59             at => $at,
60             entry => $entry,
61             });
62 27         589 return $entry->{value};
63             }
64 6         22 $entry->{_no_write} = 1;
65             # printf(STDERR "Cache-Adaptive $$ %s setting no_write\n", $entry->{build_at});
66 6         35 $self->backend->set(
67             $opts->{key},
68             $entry,
69             int($purge_after - $entry->{expires_in}));
70             }
71             } else {
72 2         13 $entry = {
73             expires_in => 0,
74             _cumu_process_time => 0,
75             _cumu_start_at => $at,
76             };
77 2         8 $entry->{_build_cnt_array}->[$purge_after - 1] = 1;
78             }
79            
80             # build
81 10         1175593 my $value = $opts->{builder}->($opts);
82 10         2000223 $entry->{process_time} = gettimeofday - $at;
83 10         37 $entry->{_cumu_process_time} += $entry->{process_time};
84 10         29 $entry->{build_at} = $at;
85 10         42 $self->_update_lifetime($entry, $opts);
86             # save
87 10         27 delete $entry->{_no_write};
88 10         69 delete $entry->{value};
89 10 100       36 $entry->{value} = $value if $entry->{expires_in};
90 10         37 $self->backend->set($opts->{key}, $entry, $purge_after);
91             # printf(STDERR "Cache-Adaptive $$ %s new entry saved\n", $at);
92             # log
93 10         2299 $self->log->({
94             %$opts,
95             type => q(miss),
96             at => $at,
97             entry => $entry,
98             });
99            
100 10         204 $value;
101             }
102              
103             sub _update_lifetime {
104 10     10   21 my ($self, $entry, $opts) = @_;
105            
106 10         153 my %params = (
107             %$self,
108             %$opts,
109             );
110 10         54 my $now = gettimeofday;
111            
112 10 50 0     59 if (! $params{check_interval}
      33        
113             || ($entry->{last_check_at} || 0) + $params{check_interval}
114             <= $now) {
115 10         33 $entry->{last_check_at} = $now;
116 10 50       48 $params{load} =
117             $entry->{_cumu_process_time} / ($now - $entry->{_cumu_start_at})
118             if $self->check_interval;
119 10         73 $entry->{_cumu_process_time} = 0;
120 10         17 $entry->{_cumu_start_at} = $now;
121 10         40 my $decision = $params{check_load}->($entry, \%params);
122 10 100       62 if ($decision > 0) { # increase
    100          
123 5 100       20 if ($entry->{expires_in}) {
124 3         31 $entry->{expires_in} = min(
125             $params{expires_max},
126             $entry->{expires_in} * $params{increase_factor});
127             } else {
128 2         8 $entry->{expires_in} = $params{expires_initial};
129             }
130             } elsif ($decision < 0) { # decrease
131 4 100       17 if ($entry->{expires_in}) {
132 3 100       15 if ($entry->{expires_in} > $params{expires_min}) {
133 2         15 $entry->{expires_in} =
134             max($params{expires_min},
135             $entry->{expires_in} * $params{decrease_factor});
136             } else {
137 1         5 $entry->{expires_in} = 0;
138             }
139             }
140             }
141             }
142            
143 10 100       63 $entry->{expires_at} =
144             $entry->{expires_in} ? $now + $entry->{expires_in} : 0;
145             }
146              
147             1;
148              
149             =head1 NAME
150              
151             Cache::Adaptive - A Cache Engine with Adaptive Lifetime Control
152              
153             =head1 SYNOPSIS
154              
155             use Cache::Adaptive;
156             use Cache::FileCache;
157            
158             my $cache = Cache::Adaptive->new({
159             backend => Cache::FileCache->new({
160             namespace => 'html_cache',
161             max_size => 10 * 1024 * 1024,
162             }),
163             expires_min => 3,
164             expires_max => 60,
165             check_load => sub {
166             my $entry = shift;
167             int($entry->{process_time} * 2) - 1;
168             },
169             });
170            
171             ...
172            
173             print "Content-Type: text/html\n\n";
174             print $cache->access({
175             key => $uri,
176             builder => sub {
177             # your HTML generation logic here
178             $html;
179             },
180             });
181              
182             =head1 DESCRIPTION
183              
184             C is a cache engine with adaptive lifetime control. Cache lifetimes can be increased or decreased by any factor, e.g. load average, process time for building the cache entry, etc., through the definition of the C callback.
185              
186             =head1 PROPERTIES
187              
188             C recognizes following properties. The properties can be set though the constructor, or by calling the accessors.
189              
190             =head2 backend
191              
192             Backend storage to be used. Should be a L object. Note: do not use Cache::SizeAwareFileCache, since its L method might overwrite data saved by other processes. The update algorithm of C needs a reliable L method.
193              
194             =head2 check_interval
195              
196             Interval between calls to the C callback for each cache entry. Default is 0, meaning that C will be called every time the cache entry is being built.
197              
198             =head2 check_load
199              
200             User supplied callback for deciding the cache policy. If a positive number is returned, cache lifetime for the entry will be increased. If a negative number is returned, the lifetime will be decreased. If 0 is returned, the lifetime will not be modified. For detail, see the L<"DEFINING THE CACHE STRATEGY"> section.
201              
202             =head2 increase_factor, decrease_factor
203              
204             Cache lifetime will be increased or decreased by applying either factor to current lifetime.
205              
206             =head2 expires_min, expires_max
207              
208             Minimal and maximal expiration times, in seconds.
209              
210             =head2 log
211              
212             An optional callback for logging.
213              
214             =head2 purge_after
215              
216             Seconds until per-entry information used for deciding caching algorithm will be purged. Defaults to C * 2.
217              
218             =head1 METHODS
219              
220             =head2 new
221              
222             See above.
223              
224             =head2 access({ key => cache_key, builder => sub { ... } })
225              
226             Returns the cached entry if possible, or builds the entry by calling the builder function, and optionally stores the build entry to cache.
227              
228             =head1 DEFINING THE CACHE STRATEGY
229              
230             A variety of cache strategies can be implemented by defining the C callback. Below are some examples.
231              
232             =head2 CACHING HEAVY OPERATIONS
233              
234             my $cache = Cache::Adaptive->new({
235             ...
236             check_load => sub {
237             my ($entry, $params) = @_;
238             int($entry->{process_time} * 2) - 1;
239             },
240             });
241              
242             Assume that the process time of each operation increases as the system becomes heavily loaded. Above code will start caching or increase cache lifetime if the process time for each operation takes more than a second. As more entries become cached, the system load will become lighter, leading to faster process times, and cache lifetimes will no more be increased. When the process time becomes smaller than 0.5 seconds, the cache lifetime will be decreased.
243              
244             =head2 CACHING FREQUENTLY ACCESSED ENTRIES
245              
246             my $cache = Cache::Adaptive->new({
247             ...
248             check_interval => 60,
249             check_load => sub {
250             my ($entry, $params) = @_;
251             int($params->{load} * 4) - 1;
252             },
253             });
254              
255             C<$params->{load}> contains C<$entry->{process_time}> divided by build frequency. The above code increases cache lifetime if the system is building the entry during more than 50% of its operation recently. Note that the system may be running multiple processes simultaneously. This value represents the C time, not CPU cycles that were actually spent for handling the operation.
256              
257             =head2 UTILIZING CACHE UNDER HEAVY LOAD
258              
259             use BSD::Sysctl qw(sysctl);
260              
261             my $cache = Cache::Adaptive->new({
262             ...
263             check_interval => 60,
264             check_load => sub {
265             my $load_avg = sysctl('vm.loadavg');
266             int($load_avg->[0] * 2) - 1;
267             },
268             });
269              
270             The example updates the cache lifetime by referring to the load average. The example should only work on BSD systems.
271              
272             =head2 A COMPLEX EXAMPLE
273              
274             my $cache = Cache::Adaptive->new({
275             ...
276             check_interval => 60,
277             check_load => sub {
278             my ($entry, $params) = @_;
279             my $load_avg = sysctl('vm.loadavg');
280             int($params{load} * 4 * $load_avg->[0] ** 2) - 1;
281             },
282             });
283              
284             The example utilizes the cache for heavily accessed entries under heavy load.
285              
286             =head1 UPDATES
287              
288             For updates, see
289              
290             http://labs.cybozu.co.jp/blog/kazuho/
291             http://labs.cybozu.co.jp/blog/kazuhoatwork/
292              
293             =head1 AUTHOR
294              
295             Copyright (c) 2007 Cybozu Labs, Inc. All rights reserved.
296              
297             written by Kazuho Oku Ekazuhooku@gmail.comE
298              
299             =head1 CONTRIBUTORS
300              
301             Toru Yamaguchi Ezigorou@cpan.orgE
302              
303             =head1 LICENSE
304              
305             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
306              
307             See http://www.perl.com/perl/misc/Artistic.html
308              
309             =cut