File Coverage

blib/lib/IO/Async/Resolver/StupidCache.pm
Criterion Covered Total %
statement 44 45 97.7
branch 6 8 75.0
condition 5 9 55.5
subroutine 9 9 100.0
pod 3 3 100.0
total 67 74 90.5


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