File Coverage

blib/lib/IO/Async/Resolver/StupidCache.pm
Criterion Covered Total %
statement 47 48 97.9
branch 6 8 75.0
condition 5 9 55.5
subroutine 10 10 100.0
pod 3 3 100.0
total 71 78 91.0


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2014-2015 -- leonerd@leonerd.org.uk
5              
6             package IO::Async::Resolver::StupidCache;
7              
8 3     3   150261 use strict;
  3         7  
  3         95  
9 3     3   12 use warnings;
  3         3  
  3         79  
10 3     3   19 use base qw( IO::Async::Notifier );
  3         3  
  3         1716  
11              
12             our $VERSION = '0.03';
13              
14 3     3   17143 use IO::Async::Resolver;
  3         164877  
  3         110  
15              
16 3     3   29 use Future 0.30; # ->without_cancel
  3         60  
  3         73  
17              
18 3     3   16 use Struct::Dumb qw( readonly_struct );
  3         6  
  3         17  
19             readonly_struct CacheEntry => [qw( future expires )];
20              
21             =head1 NAME
22              
23             C - a trivial caching layer around an C
24              
25             =head1 SYNOPSIS
26              
27             use IO::Async::Loop 0.62;
28             use IO::Async::Resolver::StupidCache;
29              
30             my $loop = IO::Async::Loop->new;
31              
32             # Wrap the existing resolver in a cache
33             $loop->set_resolver(
34             IO::Async::Resolver::StupidCache->new( source => $loop->resolver )
35             );
36              
37             # $loop->resolve requests will now be cached
38              
39             =head1 DESCRIPTION
40              
41             This object class provides a wrapper around another L
42             instance, which applies a simple caching layer to avoid making identical
43             lookups. This can be useful, for example, when performing a large number of
44             HTTP requests to the same host or a small set of hosts, or other cases where
45             it is expected that the same few resolver queries will be made over and over.
46              
47             This is called a "stupid" cache because it is made without awareness of TTL
48             values or other cache-relevant information that may be provided by DNS or
49             other resolve methods. As such, it should not be relied upon to give
50             always-accurate answers.
51              
52             =cut
53              
54             =head1 PARAMETERS
55              
56             The following named parameters may be passed to C or C:
57              
58             =over 8
59              
60             =item source => IO::Async::Resolver
61              
62             Optional. The source of the cache data. If not supplied, a new
63             C instance will be constructed.
64              
65             =item ttl => INT
66              
67             Optional. Time-to-live of cache entries in seconds. If not supplied a default
68             of 5 minutes will apply.
69              
70             =item max_size => INT
71              
72             Optional. Maximum number of entries to keep in the cache. Entries will be
73             evicted at random over this limit. If not supplied a default of 1000 entries
74             will apply.
75              
76             =back
77              
78             =cut
79              
80             sub _init
81             {
82 2     2   7982 my $self = shift;
83 2         4 my ( $params ) = @_;
84              
85 2   33     15 $params->{source} ||= IO::Async::Resolver->new;
86              
87 2   50     15 $params->{ttl} ||= 300;
88 2   50     14 $params->{max_size} ||= 1000;
89              
90 2         14 $self->SUPER::_init( $params );
91             }
92              
93             sub configure
94             {
95 2     2 1 13 my $self = shift;
96 2         5 my %params = @_;
97              
98 2         6 foreach (qw( source ttl max_size )) {
99 6 50       28 $self->{$_} = delete $params{$_} if exists $params{$_};
100             }
101              
102 2         14 $self->SUPER::configure( %params );
103             }
104              
105             =head1 METHODS
106              
107             The following methods documented with a trailing call to C<< ->get >> return
108             L instances.
109              
110             =cut
111              
112             =head2 $resolver = $cache->source
113              
114             Returns the source resolver
115              
116             =cut
117              
118             sub source
119             {
120 4     4 1 6 my $self = shift;
121 4         28 return $self->{source};
122             }
123              
124             =head2 @result = $cache->resolve( %args )->get
125              
126             =head2 @addrs = $cache->getaddrinfo( %args )->get
127              
128             =head2 ( $host, $service ) = $cache->getnameinfo( %args )->get
129              
130             These methods perform identically to the base C class,
131             except that the results are cached.
132              
133             Returned C are created with the C method, so that
134             multiple concurrent waiters are shielded from cancellation by one another.
135              
136             =cut
137              
138             sub resolve
139             {
140 13     13 1 8431 my $self = shift;
141 13         44 my %args = @_;
142              
143 13         19 my $type = $args{type};
144 13         18 my $data = $args{data};
145              
146 13   100     40 my $cache = $self->{cache} ||= {};
147              
148 13         39 my $now = $self->loop->time;
149              
150             # At the current time, all the resolvers use a flat list of non-ref scalars
151             # as arguments. We can simply flatten this to a string to use as our cache key
152              
153             # getaddrinfo needs special handling as it's a name/value pair list; accept
154             # also getaddrinfo_hash
155             my $cachekey = join "\0", ( $type =~ m/^getaddrinfo(?:_hash)?$/ )
156 13 100       171 ? do { my %data = @$data; $type, map { $_ => $data{$_} } sort keys %data }
  10         31  
  10         37  
  23         72  
157             : ( $type, @$data );
158              
159 13 100       54 if( my $entry = $cache->{$cachekey} ) {
160 9 50       34 return $entry->future->without_cancel if $entry->expires > $now;
161             }
162              
163 4         13 my $f = $self->source->resolve( %args );
164              
165 4         406 $cache->{$cachekey} = CacheEntry( $f, $now + $self->{ttl} );
166              
167 4         53 while( scalar( keys %$cache ) > $self->{max_size} ) {
168 0         0 delete $cache->{ ( keys %$cache )[rand keys %$cache] };
169             }
170              
171 4         17 return $f->without_cancel;
172             }
173              
174             # Resolver's ->getaddrinfo and ->getnameinfo convenience methods are useful to
175             # have here, but are implemented in terms of the basic ->resolve.
176             # We can cheat and just import those methods directly here
177             *getaddrinfo = \&IO::Async::Resolver::getaddrinfo;
178             *getnameinfo = \&IO::Async::Resolver::getnameinfo;
179              
180             =head1 AUTHOR
181              
182             Paul Evans
183              
184             =cut
185              
186             0x55AA;