File Coverage

File:blib/lib/Geo/Coder/Free/MissCache.pm
Coverage:96.0%

linestmtbrancondsubpodtimecode
1package 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
18our $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.
24our $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.
28my %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;
33my $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
101sub 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
128sub 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
165sub 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
197sub 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
231sub 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
267sub 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
309sub 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
321sub _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
3391;
340