File Coverage

blib/lib/Tie/Hash/MultiKeyCache.pm
Criterion Covered Total %
statement 59 68 86.7
branch 21 28 75.0
condition 10 11 90.9
subroutine 12 14 85.7
pod 4 4 100.0
total 106 125 84.8


line stmt bran cond sub pod time code
1              
2             package Tie::Hash::MultiKeyCache;
3              
4 4     4   3422 use strict;
  4         9  
  4         140  
5 4     4   21 use Carp;
  4         9  
  4         293  
6 4     4   3858 use Tie::Hash::MultiKey;
  4         23536  
  4         147  
7 4     4   76 use vars qw( $VERSION @ISA );
  4         11  
  4         6188  
8              
9             $VERSION = do { my @r = (q$Revision: 0.02 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };
10              
11             @ISA = qw( Tie::Hash::MultiKey );
12              
13             my $indexmax = 2**48; # a really big unique number that perl will not convert to float
14             my $minsize = 2; # minimum cache size
15              
16             =head1 NAME
17              
18             Tie::Hash::MultiKeyCache - aged cache or fifo
19              
20             =head1 SYNOPSIS
21              
22             This module is an extension of Tie::Hash::MultiKey and it iherits all of the methods and characteristics of
23             the parent module. Only the methods unique to this module are shown here.
24             See L for complete documentation.
25              
26             use Tie::Hash::MultiKeyCache;
27              
28             $thm = tie %h, 'Tie::Hash::MultiKeyCache',
29             SIZE => n,
30             ADDKEY => false,
31             DELKEY => false;
32             or
33              
34             $thm = tie %h, 'Tie::Hash::MultiKeyCache',
35             SIZE => n,
36             FIFO => true,
37              
38             $rv = $thm->lock($key);
39             $rv = $thm->unlock($key);
40             $size = $thm->cacheSize();
41             $oldsize = $thm->newSize();
42              
43             =head1 DESCRIPTION
44              
45             This module provides a setable fixed size CACHE implemented as a hash with
46             multiple keys per value. In normal use as new values are added to the CACHE
47             and the CACHE size is exceeded, the least used items will drop from the
48             CACHE. Particular items may be locked into the CACHE so they never expire.
49              
50             The CACHE may also be configured as a FIFO where the first items added to
51             the CACHE are the first to drop out when size is exceeded. As in the recent
52             use scenario, items LOCKED into CACHE will not be dropped.
53              
54             =over 4
55              
56             =item * $thm = tie %h, 'Tie::Hash::MultiKeyCache',
57              
58             SIZE => n,
59             ADDKEY => false, # optional
60             DELKEY => false; # optional
61             FIFO => true; # optional
62             over rides ADD,DEL KEY
63              
64             The arguments beyond the package name may be specified as a hash as shown or
65             as a reference to a hash.
66              
67             $thm = tie %h $package, { SIZE => n, options... }
68              
69             Creates a CACHE of maximum SIZE value elements and returns a method
70             pointer. Default operation refreshes cache positioning for an element
71             when a ADD Key or DELETE Key operation is performed. To disable this
72             feature, provide ADDKEY and/or DELKEY with a false value.
73              
74             input: hash,
75             cachesize
76             returns: method pointer
77              
78             The method pointer may also be accessed later with:
79              
80             $thm = tied(%h);
81              
82             =cut
83              
84             # extension data structure
85             #
86             # $self->[7] = {
87             # STACK => {
88             # vi => ai,
89             # },
90             # AI => ageindex, # incrementing number
91             # SIZE => number, # greater than 1
92             # };
93              
94             # keys of this hash are the vi's for the CACHE
95             # sort by val, zeros to the bottom, all others ascending
96             # return array of keys
97              
98             sub _sortstack {
99 7     7   11 my $stack = shift;
100             sort {
101 7 100 100     34 if ($stack->{$a} == 0 || $stack->{$b} == 0) {
  140         463  
102 57         102 $stack->{$b} <=> $stack->{$a};
103             } else {
104 83         134 $stack->{$a} <=> $stack->{$b};
105             }
106             } keys %$stack;
107             }
108              
109             sub _flush {
110 36     36   51 my $self = shift;
111 36         114 my $overflow = $self->size - $self->[7]->{SIZE};
112 36 100       262 return unless $overflow > 0;
113 7         13 my $stack = $self->[7]->{STACK};
114 7         22 my @botkeys = _sortstack($stack);
115 7         20 foreach (@botkeys) {
116 10 100       28 last unless $stack->{$_}; # stop when locked items encountered
117             # get the first key that pops out of the key hash/array
118 9         12 my $anykey = (%{$self->[2]->{$_}})[0];
  9         27  
119 9         66 $self->DELETE($anykey);
120 9 100       103 last if --$overflow < 1; # flush until out of keys or overflow
121             }
122             }
123              
124             # re-number the STACK indices if they exceed the max allowed
125             sub _scrunch {
126 0     0   0 my $self = shift;
127 0         0 my $ai = 1;
128 0         0 my $stack = $self->[7]->{STACK};
129 0         0 my @botkeys = _sortstack($self->[7]->{STACK});
130 0 0       0 my %new = map { $_, $stack->{$_} ? $ai++ : 0 } @botkeys;
  0         0  
131 0         0 $self->[7]->{STACK} = \%new;
132 0         0 $self->[7]->{AI} = $ai; # reset age index
133             }
134              
135             my $subfetch = sub {
136             my($self,$key,$vi) = @_;
137             return unless $self->[7]->{STACK}->{$vi}; # skip if locked
138             unless (exists $self->[7]->{FIFO} && $self->[7]->{FIFO}) {
139             $self->[7]->{STACK}->{$vi} = $self->[7]->{AI}++;
140             _scrunch($self) if $self->[7]->{AI} > $indexmax;
141             }
142             };
143              
144             my $substore = sub {
145             my($self,$kp,$vi) = @_;
146             $self->[7]->{STACK}->{$vi} = $self->[7]->{AI}++;
147             _flush($self);
148             _scrunch($self) if $self->[7]->{AI} > $indexmax;
149             };
150              
151             my $subdelete = sub {
152             my($self,$kp,$vp) = @_;
153             delete @{$self->[7]->{STACK}}{@{$vp}};
154             };
155              
156             my $subcopy = sub {
157             my($self,$copy,$vp) = @_;
158             @{$copy->[7]}{qw( AI SIZE )} = @{$self->[7]}{qw( AI SIZE )};
159             my $stack = $self->[7]->{STACK};
160             my %new = map { $_, $stack->{$_} } keys %$stack;
161             $copy->[7]->{STACK} = \%new;
162             };
163              
164             my $subclear = sub {
165             @{$_[0]->[7]}{qw( AI STACK )} = (1, {});
166             };
167              
168             my $subVorder = sub {
169             my($self,$kmap) = @_;
170             my $stack = $self->[7]->{STACK};
171             my %new = map { $kmap->{$_}, $stack->{$_} } keys %$kmap;
172             $self->[7]->{STACK} = \%new;
173             };
174              
175             # $kbv value => [keys]
176             # $ko keys => order
177             # $n2o new vi => [old vi order]
178             #
179             # map highest cache age (0 = max)
180             # to new vi's
181             my $subconsol = sub {
182             my($self,$kbv,$ko,$n2o) = @_;
183             my $stack = $self->[7]->{STACK};
184             my %new;
185             while (my($vi,$ovi) = each %$n2o) {
186             # foreach value, sort the old vi order by cache index
187             # to get highest old order value index.
188             # create old vi order => cache index map
189             my $ovi = (sort { # old vi -- inverse sort, max to top
190             if ( $stack->{$a} == 0 || $stack->{$b} == 0) {
191             $stack->{$a} <=> $stack->{$b};
192             } else {
193             $stack->{$b} <=> $stack->{$a};
194             }
195             } @{$n2o->{$vi}})[0];
196             $new{$vi} = $stack->{$ovi};
197             }
198             $_[0]->[7]->{STACK} = \%new;
199             };
200              
201             sub TIEHASH ($$) {
202 5     5   3219 my $self = shift;
203 5 50       35 my $args = ref $_[0] ? $_[0] : {@_};
204 5   50     29 my $size = $args->{SIZE} || 0;
205 5 50       16 croak "invalid size '$size'" if $size < $minsize; # c'mon guys....
206              
207             my $subaddkey = (exists $args->{ADDKEY} && ! $args->{ADDKEY})
208 5 100 100 1   45 ? sub {} : $subfetch;
  1         1013  
209             my $subdelkey = (exists $args->{DELKEY} && ! $args->{DELKEY})
210 5 100 100 1   33 ? sub {} : $subfetch;
  1         956  
211              
212 5         59 $self = $self->SUPER::TIEHASH(
213             FETCH => $subfetch,
214             STORE => $substore,
215             DELETE => $subdelete,
216             COPY => $subcopy,
217             CLEAR => $subclear,
218             REORDERV => $subVorder,
219             CONSOLD => $subconsol,
220             ADDKEY => $subaddkey,
221             DELKEY => $subdelkey
222             );
223 5         411 @{$self->[7]}{qw( AI SIZE STACK )} = (1,$size,{});
  5         26  
224 5 100       20 if ($args->{FIFO}) {
225 1         4 $self->[7]->{FIFO} = $args->{FIFO};
226             }
227 5         22 $self;
228             }
229              
230             =item * $rv = $thm->lock($key);
231              
232             Locks the value item into CACHE via any key in the value item's key set.
233              
234             input: any key associated with value
235             return: true on success
236             false if the key does not exist
237              
238             =cut
239              
240             sub lock {
241 7     7 1 11744 my($self,$key) = @_;
242 7 100       23 return undef unless exists $self->[0]->{$key};
243 6         12 $key = $self->[0]->{$key}; # get value index key
244 6         10 $self->[7]->{STACK}->{$key} = 0;
245 6         13 1;
246             }
247              
248             =item * $rv = $thm->unlock($key);
249              
250             Unlocks the value item via any key in the value item's key set. No operation
251             is performed if the value item is not locked in CACHE.
252              
253             input: any key associated with value
254             return: true on success
255             false if the key does not exist
256              
257             =cut
258              
259             sub unlock {
260 1     1 1 165 my($self,$key) = @_;
261 1 50       6 return undef unless exists $self->[0]->{$key};
262 1         11 $key = $self->[0]->{$key}; # get value index key
263 1         4 $self->[7]->{STACK}->{$key} = $self->[7]->{AI}++;
264 1 50       5 _scrunch($self) if $self->[7]->{AI} > $indexmax;
265 1         3 1;
266             }
267              
268             =item * $size = $thm->cacheSize;
269              
270             Returns the set size of the CACHE. This may not be the same as the number of
271             items in the CACHE. See: L $thm->size;
272              
273             input: none
274             returns: set size of the CACHE
275              
276             =cut
277              
278             sub cacheSize {
279 0     0 1 0 $_[0]->[7]->{SIZE};
280             }
281              
282             =item * $oldsize = $thm->newSize($newsize);
283              
284             Sets the maximum size of the CACHE to a new size and returns the old size. A
285             CACHE flush is performed if the new CACHE is smaller than the actual size of
286             the current CACHE. However, items locked in CACHE will not be flushed if
287             their number exceeds the new size parameter.
288              
289             =cut
290              
291             sub newSize {
292 3     3 1 6071 my $self = shift;
293 3 50       9 croak "invalid size '$_[0]'" if $_[0] < $minsize;
294 3         4 my $size = $self->[7]->{SIZE};
295 3         7 $self->[7]->{SIZE} = shift;
296 3         7 _flush($self);
297 3         7 $size;
298             }
299              
300             =head1 AUTHOR
301              
302             Michael Robinton,
303              
304             =head1 COPYRIGHT
305              
306             Copyright 2014, Michael Robinton
307              
308             This program is free software; you may redistribute it and/or modify it
309             under the same terms as Perl itself.
310              
311             This program is distributed in the hope that it will be useful,
312             but WITHOUT ANY WARRANTY; without even the implied warranty of
313             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
314              
315             =head1 SEE ALSO
316              
317             L, L
318              
319             =cut
320              
321             1;