File Coverage

blib/lib/Tie/Hash/Expire.pm
Criterion Covered Total %
statement 119 123 96.7
branch 28 34 82.3
condition 8 11 72.7
subroutine 18 18 100.0
pod 0 4 0.0
total 173 190 91.0


line stmt bran cond sub pod time code
1             package Tie::Hash::Expire;
2              
3 2     2   34089 use strict;
  2         5  
  2         83  
4              
5 2     2   1880 use POSIX qw/ceil/;
  2         16348  
  2         16  
6 2     2   2379 use Carp;
  2         7  
  2         120  
7              
8 2     2   10 use vars qw($VERSION $HI_RES_AVAILABLE);
  2         4  
  2         172  
9              
10             $VERSION = '0.03';
11              
12             BEGIN {
13 2     2   143 eval "use Time::HiRes qw/time/";
  2     2   913  
  2         1831  
  2         14  
14 2 50       371 unless($@){
15 2         2424 $HI_RES_AVAILABLE = 1;
16             }
17             }
18              
19             $Tie::Hash::Expire::clean_int = 180; # Maybe later, the user can set this.
20              
21             sub TIEHASH {
22 5     5   2556 my $class = shift;
23 5   100     23 my $args = shift || {};
24              
25             # TODO: What do we do without $args->{expire_seconds}
26 5 100       24 unless(exists $args->{expire_seconds}){
27 1         220 carp "hash tied to Tie::Hash::Expire without specifying expire_seconds. Hash keys will not expire.";
28             }
29 5 50 33     20 if(!$HI_RES_AVAILABLE and $args->{expire_seconds} =~ /\.\d+/){
30 0         0 carp "expire_seconds appears to be a decimal number, but Time::HiRes is not available.";
31             }
32              
33 5         44 my $self = {
34             'last_clean' => time,
35             'clean_int' => $Tie::Hash::Expire::clean_int,
36             'hash' => {},
37             'array' => [],
38             'lifespan' => $args->{expire_seconds},
39             };
40 5         14 bless $self, $class;
41 5         20 return $self;
42             }
43              
44             sub STORE {
45              
46 17     17   6002602 my $self = shift;
47 17         28 my $key = shift;
48 17         27 my $value = shift;
49              
50 17         51 my $time = time;
51              
52 17         43 $self->maybe_clean();
53              
54 17         53 $self->DELETE($key);
55              
56             # Insert it on the end.
57 17         25 push @{$self->{array}}, [$time,$key,$value];
  17         64  
58 17         26 $self->{hash}->{$key} = $#{$self->{array}};
  17         89  
59              
60             }
61              
62             sub FETCH {
63              
64 26     26   7608035 my $self = shift;
65 26         45 my $key = shift;
66              
67 26         68 $self->maybe_clean();
68              
69 26 100       84 if(exists $self->{hash}->{$key}){
70             # It exists, but may be expired.
71 24         56 my $time = time;
72              
73 24         51 my $index = $self->{hash}->{$key};
74 24 100 100     182 if((defined $self->{lifespan}) and $time - $self->{array}->[$index]->[0] >= $self->{lifespan}){
75             # It is expired.
76 2         14 $self->chop_hash($index);
77 2         23 return undef;
78             }
79             # It is not expired.
80 22         118 return $self->{array}->[$index]->[2];
81             } else {
82 2         9 return undef;
83             }
84             }
85              
86             sub EXISTS {
87              
88 2     2   8 my $self = shift;
89 2         3 my $key = shift;
90              
91 2         6 $self->maybe_clean();
92              
93 2 50       8 if(exists $self->{hash}->{$key}){
94             # It exists, but may be expired.
95 2         5 my $time = time;
96              
97 2         4 my $index = $self->{hash}->{$key};
98 2 100 66     17 if(defined $self->{lifespan} and $time - $self->{array}->[$index]->[0] >= $self->{lifespan}){
99             # It is expired.
100 1         4 $self->chop_hash($index);
101             }
102             }
103              
104 2         13 return exists $self->{hash}->{$key};
105              
106             }
107              
108             sub DELETE {
109              
110 18     18   749 my $self = shift;
111 18         31 my $key = shift;
112              
113 18         32 $self->maybe_clean();
114              
115 18 100       60 if(exists($self->{hash}->{$key})){
116 2         4 splice @{$self->{array}}, $self->{hash}->{$key},1;
  2         10  
117 2         11 $self->rebuild_hash();
118             }
119             }
120              
121             sub CLEAR {
122              
123 3     3   756 my $self = shift;
124              
125 3         8 $self->{hash} = {};
126 3         8 $self->{array} = [];
127 3         24 $self->{last_clean} = time;
128              
129             }
130              
131             sub FIRSTKEY {
132              
133 8     8   2003961 my $self = shift;
134 8         26 $self->clean_house();
135              
136 8 100       10 if(scalar @{$self->{array}}){
  8         24  
137 7         18 my $key = $self->{array}->[0]->[1];
138 7         12 $self->{curr_key} = 0;
139 7         127 return $key;
140             } else {
141 1         5 return undef;
142             }
143             }
144              
145             sub NEXTKEY {
146              
147 20     20   2009056 my $self = shift;
148              
149 20         44 my $chopped = $self->clean_house();
150              
151             # First, update $self->{curr_key}
152 20         44 $self->{curr_key}++;
153              
154 20 100       39 if(defined $chopped){ # The hash has changed while iterating.
155 1 50       9 if($self->{curr_key} <= $chopped){ # Start over
156 0         0 $self->{curr_key} = 0;
157             } else { # Adjust number
158 1         4 $self->{curr_key} = ($self->{curr_key}-$chopped)-1;
159             }
160             }
161              
162             # Return the right thing:
163 20 100       27 if($self->{curr_key} <= $#{$self->{array}}){
  20         47  
164 14         120 return $self->{array}->[$self->{curr_key}]->[1];
165             } else {
166 6         48 return undef;
167             }
168             }
169              
170             sub clean_house {
171              
172 28     28 0 40 my $self = shift;
173              
174             # Locate the first expired datum and chop there.
175             # Return the index of the first chopped key, or undef if no chop
176             # occurred.
177              
178 28 50       84 unless(defined $self->{lifespan}){
179 0         0 return undef;
180             }
181              
182 28         35 my $max = $#{$self->{array}};
  28         51  
183 28         42 my $min = -1;
184 28         68 my $time = time;
185 28         40 $self->{last_clean} = $time;
186              
187 28         70 while($max > $min){
188 48         230 my $try = ceil(($max+$min)/2);
189 48 100       151 if($time - $self->{array}->[$try]->[0] >= $self->{lifespan}){
190 2         9 $min = $try;
191             } else {
192 46         118 $max = $try-1;
193             }
194             }
195 28 100       55 if($min>=0){
196 2         12 $self->chop_hash($min);
197 2         11 return $min;
198             } else {
199 26         44 return undef;
200             }
201              
202             }
203              
204             sub maybe_clean {
205              
206 63     63 0 81 my $self = shift;
207              
208 63         233 my $time = time;
209 63 50       330 if($time - $self->{last_clean} >= $self->{clean_int}){
210 0         0 $self->clean_house();
211             }
212             }
213              
214             sub chop_hash {
215              
216 5     5 0 14 my $self = shift;
217 5         9 my ($index) = @_;
218              
219             # Eliminate all entries from the array at $index and before.
220              
221 5 100       7 if($index >= $#{$self->{array}}){
  5         23  
222 3         38 @{$self->{array}} = ();
  3         12  
223             } else {
224 2         8 @{$self->{array}} = @{$self->{array}}[($index+1) .. $#{$self->{array}}];
  2         12  
  2         6  
  2         6  
225             }
226              
227 5         18 $self->rebuild_hash();
228             }
229              
230             sub rebuild_hash {
231              
232 7     7 0 13 my $self = shift;
233              
234 7         50 $self->{hash} = {
235 7         16 map {$self->{array}->[$_]->[1], $_} (0..$#{$self->{array}})
  7         27  
236             };
237             }
238             1;
239              
240             __END__