| File: | blib/lib/Geo/Coder/Free/MissCache.pm |
| Coverage: | 96.0% |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package Geo::Coder::Free::MissCache; | ||||||
| 2 | |||||||
| 3 | 19 19 19 | 178 12 243 | use strict; | ||||
| 4 | 19 19 19 | 30 9 302 | use warnings; | ||||
| 5 | |||||||
| 6 | 19 19 19 | 26 12 408 | use Carp (); | ||||
| 7 | |||||||
| 8 - 16 | =head1 NAME Geo::Coder::Free::MissCache - process-level memory of lookups that found nothing =head1 VERSION Version 0.51 =cut | ||||||
| 17 | |||||||
| 18 | our $VERSION = '0.51'; | ||||||
| 19 | |||||||
| 20 | # How long a remembered miss lasts. A miss is a statement about data on disk, | ||||||
| 21 | # which does not change while a batch or a request runs; the lifetime only has | ||||||
| 22 | # to be long enough to save the repeated search, and short enough that a | ||||||
| 23 | # rebuilt data set is noticed without a restart. | ||||||
| 24 | our $EXPIRES_AFTER = 60 * 60; | ||||||
| 25 | |||||||
| 26 | # scope => { key => expiry time }. Scopes are scope_for() strings; the zero | ||||||
| 27 | # byte in them keeps a backend name from matching a data set of the same name. | ||||||
| 28 | my %misses; | ||||||
| 29 | |||||||
| 30 | # An expiry sweep costs a walk of the cache, so it runs at most this often as | ||||||
| 31 | # misses are added; missed() and size() discard what they look at regardless. | ||||||
| 32 | 19 19 19 | 45 19 6773 | use constant SWEEP_INTERVAL => 60; | ||||
| 33 | my $next_sweep = 0; | ||||||
| 34 | |||||||
| 35 - 99 | =head1 SYNOPSIS
use Geo::Coder::Free::MissCache;
my $scope = Geo::Coder::Free::MissCache->scope_for('OpenAddresses', '/data/openaddresses');
my $digest = 'AB12CD34';
# A lookup found nothing: remember that, so the next caller does not
# have to read the same database to learn it again.
Geo::Coder::Free::MissCache->remember($scope, $digest);
# A caller about to do the work asks first.
if(Geo::Coder::Free::MissCache->missed($scope, $digest)) {
print "already looked for $digest\n";
}
# The data behind the cache was rebuilt, so forget everything.
Geo::Coder::Free->clear_miss_cache();
=head1 DESCRIPTION
Every backend in C<Geo::Coder::Free> has the same problem: an address that is
not in its data is not found on every call, and each call pays for the whole
search again. C<Geo::Coder::Free::MissCache> is the one place where the
backends record those misses, so the process remembers the absence once and
every later call - through any instance of any backend - answers without
touching a database.
Entries are held in memory, in the process, and are not shared between
processes. They expire after an hour by default. That is long enough to
spare a batch, a web request or a test run the repeated work, and short enough
that a process which stays up after its data is rebuilt does not keep
answering from a stale memory forever; C<Geo::Coder::Free->clear_miss_cache()>
empties the cache at once for that case. Expired entries are discarded when
they are next asked about, and a sweep runs at most once a minute as new
misses are recorded, so the cache does not grow without bound.
A miss is only ever remembered, never inferred: nothing here decides that an
answer will be missing, it only stores that a backend has already found that
it is.
=head1 METHODS
=head2 scope_for
my $scope = Geo::Coder::Free::MissCache->scope_for('OpenAddresses', $dir);
Builds the name a backend stores its misses under. The parts are joined with
a zero byte, so the first part names the backend and the rest say which data
set is being read - two OpenAddresses directories hold different data, and a miss in
one says nothing about the other. C<clear()> uses the same convention: giving
it a backend name clears every data set that backend opened.
=head3 API SPECIFICATION
=head4 input
backend => { type => 'string' } # Local, MaxMind or OpenAddresses
dataset => { type => 'string', optional => 1 } # zero or more, which data set
=head4 output
{ type => 'string' }
=cut | ||||||
| 100 | |||||||
| 101 | sub scope_for { | ||||||
| 102 | 108 | 1 | 111 | my ($class, @parts) = @_; | |||
| 103 | |||||||
| 104 | 108 | 200 | return join("\0", @parts); | ||||
| 105 | } | ||||||
| 106 | |||||||
| 107 - 126 | =head2 missed
my $seen = Geo::Coder::Free::MissCache->missed($scope, $key);
True if C<$key> was remembered as a miss in C<$scope> and has not expired.
An expired entry is deleted as it is found. A missing entry is false and is
not an error.
=head3 API SPECIFICATION
=head4 input
scope => { type => 'string' } # from scope_for()
key => { type => 'string' }
=head4 output
{ type => 'boolean' }
=cut | ||||||
| 127 | |||||||
| 128 | sub missed { | ||||||
| 129 | 72 | 1 | 68 | my ($class, $scope, $key) = @_; | |||
| 130 | |||||||
| 131 | 72 | 113 | my $entries = $misses{$scope} or return 0; | ||||
| 132 | 50 | 48 | my $expires_at = $entries->{$key}; | ||||
| 133 | 50 | 75 | return 0 if !defined($expires_at); | ||||
| 134 | |||||||
| 135 | 13 | 17 | if($expires_at <= time()) { | ||||
| 136 | 1 | 1 | delete $entries->{$key}; | ||||
| 137 | 1 1 | 1 2 | delete $misses{$scope} if !keys %{$entries}; | ||||
| 138 | 1 | 2 | return 0; | ||||
| 139 | } | ||||||
| 140 | |||||||
| 141 | 12 | 29 | return 1; | ||||
| 142 | } | ||||||
| 143 | |||||||
| 144 - 163 | =head2 remember
Geo::Coder::Free::MissCache->remember($scope, $key);
Records that C<$key> found nothing in C<$scope>, for as long as
L</expires_after> says. Recording the same miss again refreshes it.
Returns true.
=head3 API SPECIFICATION
=head4 input
scope => { type => 'string' } # from scope_for()
key => { type => 'string' }
=head4 output
{ type => 'boolean', value => 1 }
=cut | ||||||
| 164 | |||||||
| 165 | sub remember { | ||||||
| 166 | 48 | 1 | 344 | my ($class, $scope, $key) = @_; | |||
| 167 | |||||||
| 168 | 48 | 72 | _sweep(); | ||||
| 169 | 48 | 76 | $misses{$scope}{$key} = time() + $EXPIRES_AFTER; | ||||
| 170 | |||||||
| 171 | 48 | 53 | return 1; | ||||
| 172 | } | ||||||
| 173 | |||||||
| 174 - 195 | =head2 forget
Geo::Coder::Free::MissCache->forget($scope, $key);
Forgets one miss, leaving the rest of C<$scope> alone. Returns true if there
was an entry to forget, and false if C<$scope> or C<$key> held none, so
forgetting the same miss twice returns false the second time. A backend calls
this when a lookup that had been remembered as a miss now succeeds, so a hit is
never skipped on the strength of an older miss.
=head3 API SPECIFICATION
=head4 input
scope => { type => 'string' } # from scope_for()
key => { type => 'string' }
=head4 output
{ type => 'boolean' }
=cut | ||||||
| 196 | |||||||
| 197 | sub forget { | ||||||
| 198 | 2 | 1 | 3 | my ($class, $scope, $key) = @_; | |||
| 199 | |||||||
| 200 | 2 | 11 | my $entries = $misses{$scope} or return 0; | ||||
| 201 | 1 | 3 | return 0 if !exists($entries->{$key}); | ||||
| 202 | |||||||
| 203 | 1 | 1 | delete $entries->{$key}; | ||||
| 204 | 1 1 | 1 1 | delete $misses{$scope} if !keys %{$entries}; | ||||
| 205 | |||||||
| 206 | 1 | 2 | return 1; | ||||
| 207 | } | ||||||
| 208 | |||||||
| 209 - 229 | =head2 clear
Geo::Coder::Free::MissCache->clear();
Geo::Coder::Free::MissCache->clear('OpenAddresses');
With no argument, forgets every miss in every scope. With a scope, forgets
the misses in that scope and in every scope below it: C<clear('OpenAddresses')>
empties every OpenAddresses data set this process has opened. A scope that
holds nothing, or names no backend, forgets nothing. Returns true.
=head3 API SPECIFICATION
=head4 input
scope => { type => 'string', optional => 1 } # a backend name or a scope_for() string
=head4 output
{ type => 'boolean', value => 1 }
=cut | ||||||
| 230 | |||||||
| 231 | sub clear { | ||||||
| 232 | 14 | 1 | 128692 | my ($class, $scope) = @_; | |||
| 233 | |||||||
| 234 | 14 | 17 | if(!defined($scope)) { | ||||
| 235 | 9 | 11 | %misses = (); | ||||
| 236 | 9 | 9 | return 1; | ||||
| 237 | } | ||||||
| 238 | |||||||
| 239 | 5 | 4 | my $prefix = "$scope\0"; | ||||
| 240 | 5 | 8 | foreach my $name(keys %misses) { | ||||
| 241 | 9 | 20 | delete $misses{$name} if($name eq $scope) || (index($name, $prefix) == 0); | ||||
| 242 | } | ||||||
| 243 | |||||||
| 244 | 5 | 7 | return 1; | ||||
| 245 | } | ||||||
| 246 | |||||||
| 247 - 265 | =head2 size
my $count = Geo::Coder::Free::MissCache->size();
my $count = Geo::Coder::Free::MissCache->size('Local');
How many misses are currently remembered, in all scopes or in one scope.
Expired entries are discarded first, so this is the number of live entries.
=head3 API SPECIFICATION
=head4 input
scope => { type => 'string', optional => 1 } # a backend name or a scope_for() string
=head4 output
{ type => 'integer', min => 0 }
=cut | ||||||
| 266 | |||||||
| 267 | sub size { | ||||||
| 268 | 12 | 1 | 15 | my ($class, $scope) = @_; | |||
| 269 | |||||||
| 270 | 12 | 14 | _sweep(1); | ||||
| 271 | |||||||
| 272 | my @names = defined($scope) | ||||||
| 273 | 12 2 | 15 5 | ? grep { ($_ eq $scope) || (index($_, "$scope\0") == 0) } keys %misses | ||||
| 274 | : keys %misses; | ||||||
| 275 | |||||||
| 276 | 12 | 8 | my $total = 0; | ||||
| 277 | 12 11 | 11 28 | $total += scalar(keys %{$misses{$_}}) foreach @names; | ||||
| 278 | |||||||
| 279 | 12 | 19 | return $total; | ||||
| 280 | } | ||||||
| 281 | |||||||
| 282 - 307 | =head2 expires_after
my $seconds = Geo::Coder::Free::MissCache->expires_after();
Geo::Coder::Free::MissCache->expires_after(600);
How long a remembered miss lasts, in seconds. Without an argument it answers
with the current setting, which is one hour. Setting it to 0 makes every miss
expire as soon as it is recorded, which turns the cache off. It is a setting
of the process, not of an object, like the cache itself.
The lifetime is a whole number of seconds. A negative number, a fraction or
anything that is not a number croaks
C<expires_after() expects a non-negative whole number of seconds>, and the
setting is left as it was. Returns the setting in force after the call.
=head3 API SPECIFICATION
=head4 input
seconds => { type => 'integer', min => 0, optional => 1 }
=head4 output
{ type => 'integer', min => 0 }
=cut | ||||||
| 308 | |||||||
| 309 | sub expires_after { | ||||||
| 310 | 7 | 1 | 405 | my ($class, $seconds) = @_; | |||
| 311 | |||||||
| 312 | 7 | 9 | if(defined($seconds)) { | ||||
| 313 | 4 | 17 | Carp::croak('expires_after() expects a non-negative whole number of seconds') | ||||
| 314 | unless $seconds =~ /^\d+$/; | ||||||
| 315 | 2 | 2 | $EXPIRES_AFTER = $seconds; | ||||
| 316 | } | ||||||
| 317 | |||||||
| 318 | 5 | 5 | return $EXPIRES_AFTER; | ||||
| 319 | } | ||||||
| 320 | |||||||
| 321 | sub _sweep { | ||||||
| 322 | 60 | 46 | my ($force) = @_; | ||||
| 323 | |||||||
| 324 | 60 | 46 | my $now = time(); | ||||
| 325 | 60 | 109 | return if !$force && ($now < $next_sweep); | ||||
| 326 | 19 | 18 | $next_sweep = $now + SWEEP_INTERVAL; | ||||
| 327 | |||||||
| 328 | 19 | 20 | foreach my $scope(keys %misses) { | ||||
| 329 | 12 | 10 | my $entries = $misses{$scope}; | ||||
| 330 | 12 12 | 7 10 | foreach my $key(keys %{$entries}) { | ||||
| 331 | 12 | 13 | delete $entries->{$key} if $entries->{$key} <= $now; | ||||
| 332 | } | ||||||
| 333 | 12 12 | 7 10 | delete $misses{$scope} if !keys %{$entries}; | ||||
| 334 | } | ||||||
| 335 | |||||||
| 336 | 19 | 15 | return; | ||||
| 337 | } | ||||||
| 338 | |||||||
| 339 | 1; | ||||||
| 340 | |||||||