File Coverage

blib/lib/Attribute/Cached.pm
Criterion Covered Total %
statement 57 60 95.0
branch 7 10 70.0
condition 4 11 36.3
subroutine 11 12 91.6
pod 0 4 0.0
total 79 97 81.4


line stmt bran cond sub pod time code
1             package Attribute::Cached;
2             our $VERSION = 0.02;
3              
4             =head1 NAME
5              
6             Attribute::Cached - easily cache subroutines results using a :Cached attribute
7              
8             =head1 SYNOPSIS
9              
10             sub getCache { return $global_cache }
11              
12             sub foo :Cached(60) { ... }
13             sub bar :Cached(time=>30, key=>\&keygen) { ... }
14              
15             # or supply a specific cache
16             sub baz :Cached(time=>20, cache=>$cache) { ... }
17              
18             =head1 DESCRIPTION
19              
20             In many applications, including web apps, caching data is used to help scale
21             the sites, trading a slight lack of immediacy in results with a lower load on
22             DB and other resources.
23              
24             Usually we'll do something like this
25              
26             sub my_query {
27             my ($self, %pars) = @_;
28             # get a cache
29             my $cache = $self->get_cache;
30             # generate a key: for example with %pars (foo=>1), we might use
31             # the key "my_query:foo=1";
32             my $key = $self->get_key( %pars );
33             my $result;
34             # check if we've already cached this call, and return if so
35             if ($result = $cache->get($key)) {
36             warn "Cache hit for $key";
37             return $result;
38             }
39             # The next lines are what this subroutine is /actually/ doing
40             $result = $self->expensive_operation;
41             # ... additional processing as required
42              
43             # set the result in the cache for future accesses
44             $cache->set($key, $result, 20); # hard code a cache time here
45              
46             return $result;
47             }
48              
49             The caching logic is repeated boilerplate and, worse, really has nothing
50             to do with what we're trying to achieve here. With L
51             we'd write this as:
52              
53             sub getCache { my $self = shift; return $self->get_cache(@_) }
54              
55             sub my_query :Cached(time=>20, key=>\&get_key) {
56              
57             my $result = $self->expensive_operation;
58             # ... additional processing as required
59              
60             return $result;
61             }
62              
63             =head1 ATTRIBUTE VALUES
64              
65             The C<:Cached> attribute takes the following parameters
66              
67             =over 4
68              
69             =item C
70              
71             The cache time. This is often a value in seconds. But some cache interfaces
72             require a string like "5 secs". Either an integer or any expression parseable
73             by L can be passed in (for example a constant).
74              
75             If time is the only attribute required, the shortcut form C<:Cached(CACHE_TIME)>
76             is supported too. Alternatively, see the hook C to set this
77             dynamically.
78              
79             =item C
80              
81             The cache must be a "standard" type, conforming to the same interface
82             as C. That is, it should have the usual C and C
83             methods. Specifics can vary (like Cachetime handling, which is specified
84             differently for memcached).
85              
86             If there is a default cache set in a global variable, you can pass it
87             in like so
88              
89             :Cached(cache=>$cache)
90              
91             Most likely you will want to define the hook C instead.
92              
93             =item C
94              
95             This is a method name or subroutine reference that will generate the
96             appropriate key. There is a default behaviour for this, but it is
97             to join all arguments with commas (including the stringified $self,
98             which is likely not what you want. So this default behaviour may
99             be subject to change in future versions.)
100              
101             The method is dispatched via the package name, and will be passed
102              
103             - package name
104             - subroutine name
105             - original args passed (including $self if this is an OO method)
106              
107             If you wanted a single cache key, you could always use
108             C<:Cached(key=>sub{'foo'}})>.
109              
110             If all the methods in your package use the same keygen, you could
111             define the L hook instead.
112              
113             =item C
114              
115             Usually caches set and return a single scalar value. The subroutine
116             you want to clean up using this module might have had logic with
117             C for example. Setting a transform subroutine lets you do
118             this.
119              
120             sub refOrArray { wantarray ? @$_[0] : $_[0]; }
121             sub foo :Cached(time=>20, transform=>\&refOrArray) { ... }
122              
123             You cannot pass a method name to be dispatched (for what seemed like
124             good reasons at the time: patches welcome if that's sufficiently annoying
125             to anyone). However you can define a global hook C
126             for your package.
127              
128             =back
129              
130             =head2 Hooks
131              
132             You can define several methods in your class or base class to
133             avoid having to type repeated code.
134              
135             =over 4
136              
137             =item C
138              
139             Define this method to return a cache time dynamically. The package
140             and subroutine name are prepended to the original arguments.
141              
142             sub getCacheTime {
143             my ($package, $subname, %args) = @_;
144             return 20 if $subname eq 'query';
145             return 60;
146             }
147              
148             =item C
149              
150             Define this method to return a cache (of the sort specified under L
151             above.
152              
153             Only the original arguments are passed. (This behaviour may change).
154             For example, for a Catalyst method which is passed ($self, $c, %args) you might
155             do:
156              
157             sub getCache {
158             my ($self, $c) = @_;
159             return $c->model('Cache');
160             }
161              
162             =item C
163              
164             Define this method to determine the cache key for the method call.
165             As we don't know whether we're dealing with a sub or a method call,
166             the default implementation doesn't try to do anything clever. For
167             now you'd probably want to define something like this:
168              
169             sub getCacheKey {
170             my ($package, $subname, $self, %args) = @_;
171             return join ':', $package, $subname,
172             map { "$_=$args{$_}" } keys %args;
173             }
174              
175             The default behaviour may change.
176              
177             =item C
178              
179             This is the analogue to the C parameter above.
180              
181             =back
182              
183             =cut
184              
185 1     1   46251 use warnings;
  1         3  
  1         34  
186 1     1   6 use strict;
  1         2  
  1         44  
187 1     1   1664 use Attribute::Handlers;
  1         8337  
  1         7  
188              
189 1     1   112 use constant DEBUG=>0;
  1         2  
  1         354  
190              
191             sub UNIVERSAL::Cached :ATTR(CODE) {
192 1     1 0 3399 my ($pkg, $symbol, $options) = @_[0,1,4];
193              
194 1         3 my %config;
195 1 50       5 if (ref $options eq 'ARRAY') {
196 1         4 %config = @$options
197             } else {
198 0         0 %config = (time => $options);
199             }
200 1         2 my $name = *{$symbol}{NAME};
  1         3  
201 1         1 my $code = *{$symbol}{CODE};
  1         2  
202              
203 1         5 my $sub = encache($pkg, $name, $code, %config);
204 1         5 my $subname = "${pkg}::${name}";
205 1         2 warn "Installing into $subname" if DEBUG;
206 1     1   6 no strict 'refs';
  1         2  
  1         32  
207 1     1   7 no warnings 'redefine';
  1         2  
  1         65  
208 1         1 *{$subname} = $sub;
  1         8  
209 1     1   6 }
  1         2  
  1         6  
210              
211             sub encache {
212 2     2 0 35 my ($pkg, $name, $code, %config) = @_;
213 2 50 33     12 return unless my $ct
214             = $config{time} || $pkg->can('getCacheTime');
215              
216 2         3 warn "code is $name, $code" if DEBUG;
217              
218 2   33     29 my $getCache = $config{cache} || $pkg->can('getCache');
219 2   50     9 my $getCacheKey = $config{key}
220             || $pkg->can('getCacheKey')
221             || \&getCacheKeyDefault;
222 2   33     24 my $transform = $config{transform} || $pkg->can('cacheTransform');
223              
224             my $sub = sub {
225             # give the anonymous sub a name
226             # (alternatively, use Sub::Named, as suggested by Ash)
227 6     6   12574 local *__ANON__ = "Cached($name)";
228 6         21 my $cache = literalOrCall($getCache, @_);
229 6         136 my $key = $pkg->$getCacheKey( $name, @_ );
230              
231 6         47 my $result = $cache->get( $key );
232 6 100       68 if ($result) {
233 2         3 warn "Cache($name) hit for $key => $result" if DEBUG;
234             } else {
235 4         5 warn "Cache($name) miss for $key" if DEBUG;
236 4         15 $result = $code->(@_);
237             # we could have been passed a subroutine!
238 4         24 my $cachetime = literalOrCall($ct, $pkg, $name, @_);
239 4         7 warn "Cache($name) Setting $key => $result ($cachetime)" if DEBUG;
240 4         16 $cache->set( $key, $result, $cachetime );
241             }
242 6 50       60 return $result unless $transform;
243 0         0 return $transform->($result, @_);
244 2         23 };
245 2         87 return $sub;
246             }
247              
248             sub getCacheKeyDefault {
249 0     0 0 0 return join ';' => @_;
250             }
251             sub literalOrCall {
252 10     10 0 17 my $what = shift;
253 10 100       38 return $what unless ref $what eq 'CODE';
254 6         19 return $what->(@_);
255             }
256              
257             1;
258              
259             =head1 PERFORMANCE
260              
261             Automatically wrapping the caching logic requires a slightly generic approach
262             which may not be optimal. The bundled C program tries to
263             quantify this. In a sample run of 1,000,000 iterations, it can be seen that
264             the additional work requires approximately 10 millionths of a second per
265             iteration. This is likely to be fast enough for most requirements.
266              
267             Using the Attribute::Handling (instead of manually using the C
268             subroutine which does the actual work) appears to be a tiny fraction of the
269             total overhead (1 millionth of a second per iteration).
270              
271             (Benchmark results on my machine, please give me a shout if you get wildly
272             different results).
273              
274             =head1 SEE ALSO
275              
276             The attribute code is "inspired" by L, and uses the very
277             funky L. This latter seems to be full of very tasty
278             crack, but is also much nicer than doing the attribute parsing ourselves.
279              
280             You'll need a caching module like L or L.
281              
282             The wrapping might be done better with L
283              
284             =head1 STATUS and BUGS
285              
286             This is version 0.01, in alpha. The interface is likely to
287             change, as indicated in several places in comments in the above
288             POD. Please get in touch if you have suggestions or concerns
289             about the public API.
290              
291             Please report via RT on cpan, or to L.
292              
293             Or grab osfameron on IRC, for example on C
294              
295             =head1 AUTHOR and LICENSE
296              
297             By osfameron, for Thermeon Ltd.
298              
299             (C)2007 Thermeon Europe
300              
301             This program is free software, you can redistribute it and/or modify it under
302             the same terms as Perl itself.
303              
304             =cut