File Coverage

blib/lib/Cache/BDB.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Cache::BDB;
2              
3 3     3   98261 use strict;
  3         6  
  3         107  
4 3     3   17 use warnings;
  3         5  
  3         87  
5              
6 3     3   3147 use BerkeleyDB;
  0            
  0            
7             use Storable;
8             use File::Path qw(mkpath);
9              
10             our $VERSION = '0.04';
11              
12             use constant DEFAULT_DB_TYPE => 'Btree';
13              
14             #############################
15             # Construction/Destruction. #
16             #############################
17            
18             sub new {
19             my ($proto, %params) = @_;
20             my $class = ref($proto) || $proto;
21              
22             die "$class requires Berkeley DB version 3 or greater"
23             unless $BerkeleyDB::db_version >= 3;
24            
25             # can't do anything without at least these params
26             die "$class: cache_root not specified" unless($params{cache_root});
27             die "$class: namespace not specified" unless($params{namespace});
28              
29             my $cache_root = $params{cache_root};
30             unless(-d $cache_root) {
31             eval {
32             mkpath($cache_root, 0, 0777);
33             };
34             if($@) {
35             die "$class: cache_root '$cache_root' unavailable: $@";
36             }
37             }
38            
39             my $env = BerkeleyDB::Env->new(
40             -Home => $cache_root,
41             -Flags =>
42             (DB_INIT_CDB | DB_CREATE | DB_INIT_MPOOL),
43             -ErrPrefix => $class,
44             -ErrFile => *STDERR,
45             -SetFlags =>
46             $params{env_lock} ? DB_CDB_ALLDB : 0,
47             -Verbose => 1,
48             )
49             or die "$class: Unable to create env: $BerkeleyDB::Error";
50              
51             my $type = join('::', 'BerkeleyDB', ($params{type} &&
52             ($params{type} eq 'Btree' ||
53             $params{type} eq 'Hash' ||
54             $params{type} eq 'Recno')) ?
55             $params{type} : DEFAULT_DB_TYPE);
56              
57             my $fname = $params{cache_file} || join('.', $params{namespace}, "db");
58              
59             my $db = $type->new(
60             -Env => $env,
61             -Subname => $params{namespace},
62             -Filename => $fname,
63             -Flags => DB_CREATE,
64             # -Pagesize => 8192,
65             );
66              
67             # make a second attempt to connect to the db. this should handle
68             # the case where a cache was created with one type and connected
69             # to again with a different type. should probably just be an
70             # error, but just in case ...
71            
72             unless(defined $db ) {
73             $db = BerkeleyDB::Unknown->new(
74             -Env => $env,
75             -Subname => $params{namespace},
76             -Filename => $fname,
77             #-Pagesize => 8192,
78             );
79             }
80            
81             die "$class: Unable to open db: $BerkeleyDB::Error" unless defined $db;
82            
83             # eventually these should support user defined subs and/or
84             # options as well.
85             $db->filter_store_value( sub { $_ = Storable::freeze($_) });
86             $db->filter_fetch_value( sub { $_ = Storable::thaw($_) });
87            
88             # sync the db for good measure.
89             $db->db_sync();
90            
91             my $self = {
92             # private stuff
93             __env => $env,
94             __last_purge_time => time(),
95             __type => $type,
96             __db => $db,
97              
98             # expiry/purge
99             default_expires_in => $params{default_expires_in} || 0,
100             auto_purge_interval => $params{auto_purge_interval} || 0,
101             auto_purge_on_set => $params{auto_purge_on_set} || 0,
102             auto_purge_on_get => $params{auto_purge_on_get} || 0,
103              
104             purge_on_init => $params{purge_on_init} || 0,
105             purge_on_destroy => $params{purge_on_destroy} || 0,
106              
107             clear_on_init => $params{clear_on_init} || 0,
108             clear_on_destroy => $params{clear_on_destroy} || 0,
109              
110             disable_auto_purge => $params{disable_auto_purge} || 0,
111              
112             # file/namespace
113             namespace => $params{namespace},
114             cache_root => $params{cache_root},
115              
116             # options
117             disable_compact => $params{disable_compact},
118              
119             };
120              
121             bless $self, $class;
122            
123             $self->clear() if $self->{clear_on_init};
124             $self->purge() if $self->{purge_on_init};
125              
126             return $self;
127             }
128              
129             sub DESTROY {
130             my $self = shift;
131              
132             $self->clear() if $self->{clear_on_destroy};
133             $self->purge() if $self->{purge_on_destroy};
134              
135             undef $self->{__db};
136             undef $self->{__env};
137             }
138              
139             *close = \&DESTROY;
140              
141             ##############################################
142             # Instance options and expiry configuration. #
143             ##############################################
144              
145             sub namespace {
146             my $self = shift;
147             warn "namespace is read only" if shift;
148             return $self->{namespace};
149             }
150              
151             sub auto_purge_interval {
152             my ($self, $interval) = @_;
153              
154             if(defined($interval)) {
155             return undef unless $interval =~ /^\d+$/;
156             $self->{auto_purge_interval} = $interval;
157             }
158             return $self->{auto_purge_interval};
159             }
160              
161             sub auto_purge_on_set {
162             my ($self, $v) = @_;
163             if(defined($v)) {
164             $self->{auto_purge_on_set} = $v;
165             }
166             return $self->{auto_purge_on_set};
167             }
168              
169             sub auto_purge_on_get {
170             my ($self, $v) = @_;
171             if(defined($v)) {
172             $self->{auto_purge_on_get} = $v;
173             }
174             return $self->{auto_purge_on_get};
175             }
176              
177             #################################################
178             # Methods for setting and getting cached items. #
179             #################################################
180              
181             sub set {
182             my ($self, $key, $value, $ttl) = @_;
183              
184             return 0 unless ($key && $value);
185              
186             my $db = $self->{__db};
187             my $rv;
188              
189             my $now = time();
190              
191             if($self->{auto_purge_on_set}) {
192             my $interval = $self->{auto_purge_interval};
193             if($now > ($self->{__last_purge_time} + $interval)) {
194             $self->purge();
195             $self->{__last_purge_time} = $now;
196             }
197             }
198            
199             $ttl ||= $self->{default_expires_in};
200             my $expires = ($ttl) ? $now + $ttl : 0;
201            
202             my $data = {__expires => $expires,
203             __set_time => $now,
204             __last_access_time => $now,
205             __version => $Cache::BDB::VERSION,
206             __data => $value};
207              
208             $rv = $db->db_put($key, $data);
209              
210             return $rv ? 0 : 1;
211             }
212              
213             sub add {
214             my ($self, $key, $value, $ttl) = @_;
215              
216             return $self->get($key) ? 0 : $self->set($key, $value, $ttl);
217             }
218              
219             sub replace {
220             my ($self, $key, $value, $ttl) = @_;
221              
222             return $self->get($key) ? $self->set($key, $value, $ttl) : 0;
223             }
224              
225             sub get {
226             my ($self, $key) = @_;
227              
228             return undef unless $key;
229             my $db = $self->{__db};
230             my $t = time();
231              
232             my $data;
233              
234             if($self->{auto_purge_on_get}) {
235             my $interval = $self->{auto_purge_interval};
236             if($t > ($self->{__last_purge_time} + $interval)) {
237             $self->purge();
238             $self->{__last_purge_time} = $t;
239             }
240             }
241            
242             my $rv = $db->db_get($key, $data);
243             return undef if $rv == DB_NOTFOUND;
244             return undef unless $data->{__data};
245              
246             if($self->__is_expired($data, $t)) {
247             $self->remove($key) unless $self->{disable_auto_purge};
248             return undef;
249             }
250             # this is pretty slow, leaving it out for now. if i start supporting
251             # access time related stuff i'll need to work on it.
252             # $self->_update_access_time($key, $data, $t);
253            
254             return $data->{__data};
255             }
256              
257             sub get_bulk {
258             my $self = shift;
259             my $t = time();
260             my $count = 0;
261            
262             my $db = $self->{__db};
263             my $cursor = $db->db_cursor();
264            
265             my %ret;
266             my ($k, $v) = ('','');
267              
268             while($cursor->c_get($k, $v, DB_NEXT) == 0) {
269             my $d = $self->get($k);
270             $ret{$k} = $d if $d;
271             }
272             $cursor->c_close();
273              
274             return \%ret;
275             }
276              
277             sub _update_access_time {
278             my ($self, $key, $data, $t) = @_;
279            
280             my $db = $self->{__db};
281             $t ||= time();
282             $data->{__last_access_time} = $t;
283              
284             my $rv = $db->db_put($key, $data);
285            
286             return $rv;
287             }
288              
289             ###########################
290             # Cache meta information. #
291             ###########################
292              
293             sub count {
294             my $self = shift;
295             my $total = 0;
296              
297             my $db = $self->{__db};
298             my $stats = $db->db_stat;
299             my $type = $db->type;
300              
301             $total = ($type == DB_HASH) ?
302             $stats->{hash_ndata} : $stats->{bt_ndata};
303              
304             return $total;
305             }
306              
307             sub size {
308             my $self = shift;
309            
310             my $db = $self->{__db};
311              
312             eval { require Devel::Size };
313             if($@) {
314             warn "size() currently requires Devel::Size";
315             return 0;
316             }
317             else {
318             import Devel::Size qw(total_size);
319             }
320            
321             my ($k, $v) = ('','');
322             my $size = 0;
323              
324             my $cursor = $self->{__db}->db_cursor();
325             while($cursor->c_get($k, $v, DB_NEXT) == 0) {
326             $size += total_size($v->{__data});
327             }
328              
329             $cursor->c_close();
330             return $size;
331             }
332              
333             ##############################################
334             # Methods for removing items from the cache. #
335             ##############################################
336              
337             sub remove {
338             my ($self, $key) = @_;
339              
340             my $rv;
341             my $v = '';
342             my $db = $self->{__db};
343             $rv = $db->db_del($key);
344              
345             warn "compaction failed!" if $self->_compact();
346              
347             return $rv ? 0 : 1;
348             }
349              
350             *delete = \&remove;
351              
352             sub clear {
353             my $self = shift;
354             my $rv;
355              
356             my $count = 0;
357             my $db = $self->{__db};
358             $rv = $db->truncate($count);
359              
360             warn "compaction failed!" if $self->_compact();
361              
362             return $count;
363             }
364              
365             sub purge {
366             my $self = shift;
367             my $t = time();
368             my $count = 0;
369            
370             my $db = $self->{__db};
371             my $cursor = $db->db_cursor(DB_WRITECURSOR);
372              
373             my ($k, $v) = ('','');
374             while($cursor->c_get($k, $v, DB_NEXT) == 0) {
375             if($self->__is_expired($v, $t)) {
376             $cursor->c_del();
377             $count++;
378             }
379             }
380             $cursor->c_close();
381              
382             warn "compaction failed!" if $self->_compact();
383              
384             return $count;
385             }
386              
387             sub __is_expired {
388             my ($self, $data, $t) = @_;
389             $t ||= time();
390              
391             return 1 if($data->{__expires} && $data->{__expires} < $t);
392             return 0;
393             }
394              
395             sub is_expired {
396             my ($self, $key) = @_;
397              
398             my $data;
399             my $t = time();
400             return 0 unless $key;
401             my $db = $self->{__db};
402             my $rv = $db->db_get($key, $data);
403              
404             return 0 unless $data;
405             return $self->__is_expired($data, $t);
406             }
407              
408             sub _compact {
409             my $self = shift;
410              
411             my $rv = 0; # assume success, if compact isn't available pretend its cool
412             my $db = $self->{__db};
413             if($db->can('compact') &&
414             $db->type == DB_BTREE &&
415             !$self->{disable_compact}) {
416             $rv = $db->compact(undef, undef, undef, DB_FREE_SPACE, undef);
417             }
418             return $rv;
419             }
420              
421             1;