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 |