File Coverage

blib/lib/CHI/Driver/SharedMem.pm
Criterion Covered Total %
statement 138 161 85.7
branch 25 42 59.5
condition 6 12 50.0
subroutine 27 27 100.0
pod 10 10 100.0
total 206 252 81.7


line stmt bran cond sub pod time code
1             package CHI::Driver::SharedMem;
2              
3             # There is an argument for mapping namespaces into keys and then putting
4             # different namespaces into different shared memory areas. I will think about
5             # that.
6              
7 5     5   676191 use warnings;
  5         24  
  5         189  
8 5     5   32 use strict;
  5         11  
  5         122  
9 5     5   1940 use CHI::Constants qw(CHI_Meta_Namespace);
  5         1970  
  5         275  
10 5     5   2882 use Moose;
  5         2440232  
  5         34  
11 5     5   41234 use IPC::SysV qw(S_IRUSR S_IWUSR IPC_CREAT);
  5         2526  
  5         504  
12 5     5   1026 use IPC::SharedMem;
  5         10492  
  5         184  
13 5     5   545 use JSON::MaybeXS;
  5         5851  
  5         350  
14 5     5   45 use Carp;
  5         22  
  5         290  
15 5     5   40 use Config;
  5         10  
  5         266  
16 5     5   73 use Fcntl;
  5         12  
  5         10637  
17              
18             extends 'CHI::Driver';
19              
20             has 'shm_key' => (is => 'ro', isa => 'Int');
21             has 'shm' => (is => 'ro', builder => '_build_shm', lazy => 1);
22             has 'shm_size' => (is => 'rw', isa => 'Int', default => 8 * 1024);
23             has 'lock' => (
24             is => 'ro',
25             builder => '_build_lock',
26             );
27             has 'lock_file' => (is => 'rw', isa => 'Str|Undef');
28             has '_data_size' => (
29             is => 'rw',
30             isa => 'Int',
31             reader => '_get_data_size',
32             writer => '_set_data_size'
33             );
34             has '_data' => (
35             is => 'rw',
36             # isa => 'ArrayRef[ArrayRef]', # For Storable, now using JSON
37             isa => 'Str',
38             reader => '_get_data',
39             writer => '_set_data'
40             );
41              
42             __PACKAGE__->meta->make_immutable();
43              
44             =head1 NAME
45              
46             CHI::Driver::SharedMem - Cache data in shared memory
47              
48             =head1 VERSION
49              
50             Version 0.18
51              
52             =cut
53              
54             our $VERSION = '0.18';
55              
56             # FIXME - get the pod documentation right so that the layout of the memory
57             # area looks correct in the man page
58              
59             =head1 SYNOPSIS
60              
61             L<CHI> driver which stores data in shared memory objects for persistence
62             over processes.
63             Size is an optional parameter containing the size of the shared memory area,
64             in bytes.
65             Shmkey is a mandatory parameter containing the IPC key for the shared memory
66             area.
67             See L<IPC::SharedMem> for more information.
68              
69             use CHI;
70             my $cache = CHI->new(
71             driver => 'SharedMem',
72             max_size => 2 * 1024, # Size of the cache
73             shm_size => 32 * 1024, # Size of the shared memory area
74             shm_key => 12344321, # Choose something unique, but the same across
75             # all caches so that namespaces will be shared,
76             # but we won't step on any other shm areas
77             );
78             # ...
79              
80             The shared memory area is stored thus:
81              
82             # Number of bytes in the cache [ int ]
83             'cache' => {
84             'namespace1' => {
85             'key1' => 'value1',
86             'key2' => 'value2',
87             # ...
88             },
89             'namespace2' => {
90             'key1' => 'value3',
91             'key3' => 'value2',
92             # ...
93             }
94             # ...
95             }
96              
97             =head1 SUBROUTINES/METHODS
98              
99             =head2 store
100              
101             Stores an object in the cache.
102             The data are serialized into JSON.
103              
104             =cut
105              
106             sub store {
107 548     548 1 329057 my($self, $key, $value) = @_;
108              
109 548         1655 $self->_lock(type => 'write');
110 548         1136 my $h = $self->_data();
111 548         2378 $h->{$self->namespace()}->{$key} = $value;
112             # if($self->{'is_size_aware'}) {
113             # $h->{CHI_Meta_Namespace()}->{'last_used_time'}->{$key} = time;
114             # }
115 548         1471 $self->_data($h);
116 547         1308 $self->_unlock();
117             }
118              
119             =head2 fetch
120              
121             Retrieves an object from the cache
122              
123             =cut
124              
125             sub fetch {
126 1785     1785 1 792994 my($self, $key) = @_;
127              
128             # open(my $tulip, '>>', '/tmp/tulip');
129             # print $tulip __LINE__, "\n";
130 1785         4955 $self->_lock(type => 'read');
131             # print $tulip __LINE__, "\n";
132 1785         3627 my $rc = $self->_data()->{$self->namespace()}->{$key};
133             # print $tulip __LINE__, "\n";
134 1785 100       9898 if($self->{is_size_aware}) {
135 367         1055 $self->_lock(type => 'write');
136 367         755 my $h = $self->_data();
137 367         975 $h->{CHI_Meta_Namespace()}->{last_used_time}->{$key} = time;
138 367         762 $self->_data($h);
139             }
140 1785         4730 $self->_unlock();
141             # print $tulip __LINE__, "\n";
142             # close $tulip;
143 1785         4694 return $rc;
144             }
145              
146             =head2 remove
147              
148             Remove an object from the cache
149              
150             =cut
151              
152             sub remove {
153 132     132 1 154745 my($self, $key) = @_;
154              
155 132         454 $self->_lock(type => 'write');
156 132         271 my $h = $self->_data();
157 132         551 delete $h->{$self->namespace()}->{$key};
158 132         291 delete $h->{CHI_Meta_Namespace()}->{last_used_time}->{$key};
159 132         393 $self->_data($h);
160 132         323 $self->_unlock();
161              
162             # open(my $tulip, '>>', '/tmp/tulip');
163             # print $tulip "remove: $key\n";
164             }
165              
166             =head2 clear
167              
168             Removes all data from the current namespace
169              
170             =cut
171              
172             sub clear {
173 109     109 1 64039 my $self = shift;
174              
175 109         366 $self->_lock(type => 'write');
176 109         251 my $h = $self->_data();
177 109         623 delete $h->{$self->namespace()};
178 109         302 $self->_data($h);
179 109         288 $self->_unlock();
180              
181             # open(my $tulip, '>>', '/tmp/tulip');
182             # print $tulip "clear ", $self->namespace(), "\n";
183             }
184              
185             =head2 get_keys
186              
187             Gets a list of the keys in the current namespace
188              
189             =cut
190              
191             sub get_keys {
192 126     126 1 59764 my $self = shift;
193              
194 126         372 $self->_lock(type => 'read');
195 126         267 my $h = $self->_data();
196 126         417 $self->_unlock();
197 126         175 return(keys(%{$h->{$self->namespace()}}));
  126         1634  
198             }
199              
200             =head2 get_namespaces
201              
202             Gets a list of the namespaces in the cache
203              
204             =cut
205              
206             sub get_namespaces {
207 12     12 1 10643 my $self = shift;
208              
209 12         49 $self->_lock(type => 'read');
210 12         31 my $rc = $self->_data();
211 12         41 $self->_unlock();
212             # Needs to be sorted for RT89892
213 12         15 my @rc = sort keys(%{$rc});
  12         67  
214 12         103 return @rc;
215             }
216              
217             =head2 default_discard_policy
218              
219             Use an LRU algorithm to discard items when the cache can't add anything
220              
221             =cut
222              
223 4     4 1 325 sub default_discard_policy { 'lru' }
224              
225             =head2 discard_policy_lru
226              
227             When the Shared memory area is getting close to full, discard the least recently used objects
228              
229             =cut
230              
231             sub discard_policy_lru {
232             # return; # debugging why I get uninitialized values in the sort
233 14     14 1 912 my $self = shift;
234              
235 14         49 $self->_lock(type => 'read');
236 14         33 my $last_used_time = $self->_data()->{CHI_Meta_Namespace()}->{last_used_time};
237 14         103 $self->_unlock();
238             my @keys_in_lru_order =
239 14         51 sort { $last_used_time->{$a} <=> $last_used_time->{$b} } $self->get_keys();
  192         333  
240             return sub {
241 36     36   4260 shift(@keys_in_lru_order);
242 14         118 };
243             }
244              
245             # Internal routines
246              
247             # The area must be locked by the caller
248             sub _build_shm {
249 97     97   224 my $self = shift;
250 97         2755 my $shm_size = $self->shm_size();
251              
252 97 50 33     440 if((!defined($shm_size)) || ($shm_size == 0)) {
253             # Probably some strange condition in cleanup
254             # croak 'Size == 0';
255 0         0 return;
256             }
257 97         2709 my $shm = IPC::SharedMem->new($self->shm_key(), $shm_size, S_IRUSR|S_IWUSR);
258 97 100       3008 unless($shm) {
259 4         137 $shm = IPC::SharedMem->new($self->shm_key(), $shm_size, S_IRUSR|S_IWUSR|IPC_CREAT);
260 4 50       525 unless($shm) {
261 0         0 croak "Couldn't create a shared memory area of $shm_size bytes with key ",
262             $self->shm_key(), ": $!";
263 0         0 return;
264             }
265 4         58 $shm->write(pack('I', 0), 0, $Config{intsize});
266             }
267 97         834 $shm->attach();
268 97         6783 return $shm;
269             }
270              
271             sub _build_lock {
272 99     99   520319 return;
273              
274 0         0 my $self = shift;
275              
276             # open(my $fd, '<', $0) || croak("$0: $!");
277             # FIXME: make it unique for each object, not a singleton
278 0         0 $self->lock_file('/tmp/' . __PACKAGE__);
279             # open(my $tulip, '>>', '/tmp/tulip');
280             # print $tulip "build_lock\n", $self->lock_file(), "\n";
281 0 0       0 open(my $fd, '>', $self->lock_file()) || croak($self->lock_file(), ": $!");
282 0         0 return $fd;
283             }
284              
285             sub _lock {
286 3190     3190   4643 return;
287              
288 0         0 my ($self, %params) = @_;
289              
290             # open(my $tulip, '>>', '/tmp/tulip');
291             # print $tulip $params{'type'}, ' lock ', $self->lock_file(), "\n";
292             # my $i = 0;
293             # while((my @call_details = (caller($i++)))) {
294             # print $tulip "\t", $call_details[1], ':', $call_details[2], ' in function ', $call_details[3], "\n";
295             # }
296 0 0       0 return unless $self->lock_file();
297              
298 0 0       0 if(my $lock = $self->lock()) {
299 0 0       0 flock($lock, ($params{type} eq 'read') ? Fcntl::LOCK_SH : Fcntl::LOCK_EX);
300             } else {
301             # print $tulip 'lost lock ', $self->lock_file(), "\n";
302 0         0 croak('Lost lock: ', $self->lock_file());
303             }
304             }
305              
306             sub _unlock {
307 2822     2822   10224 return;
308              
309 0         0 my $self = shift;
310              
311             # open(my $tulip, '>>', '/tmp/tulip');
312             # print $tulip 'unlock ', $self->lock_file(), "\n";
313 0 0       0 if(my $lock = $self->lock()) {
314 0         0 flock($lock, Fcntl::LOCK_UN);
315             } else {
316             # print $tulip 'lost lock for unlock ', $self->lock_file(), "\n";
317 0         0 croak('Lost lock for unlock: ', $self->lock_file());
318             }
319             }
320              
321             # The area must be locked by the caller
322             sub _data_size {
323 4345     4345   7036 my($self, $value) = @_;
324              
325 4345 100       8501 if(defined($value)) {
326 1155         32355 $self->shm()->write(pack('I', $value), 0, $Config{intsize});
327 1155         19043 return $value;
328             }
329 3190 50       98468 unless($self->shm()) {
330 0         0 return 0;
331             }
332 3190         83140 my $size = $self->shm()->read(0, $Config{intsize});
333 3190 50       71671 unless(defined($size)) {
334 0         0 return 0;
335             }
336 3190         9114 return unpack('I', $size);
337             }
338              
339             # The area must be locked by the caller
340             sub _data {
341 4249     4249   8062 my($self, $h) = @_;
342              
343             # open(my $tulip, '>>', '/tmp/tulip');
344             # print $tulip __LINE__, "\n";
345 4249 100       9234 if(defined($h)) {
346 1156         3307 my $f = JSON::MaybeXS->new()->ascii()->encode($h);
347 1156         55466 my $cur_size = length($f);
348             # print $tulip __LINE__, "cmp $cur_size > ", $self->size(), "\n";
349 1156 100       38434 if($cur_size > ($self->shm_size() - $Config{intsize})) {
350 1         33 croak("sharedmem set failed - value too large? ($cur_size bytes) > ", $self->shm_size());
351             }
352 1155         31934 $self->shm()->write($f, $Config{intsize}, $cur_size);
353 1155         23686 $self->_data_size($cur_size);
354             # print $tulip "set: $cur_size bytes\n";
355             # close $tulip;
356 1155         4257 return $h;
357             }
358 3093         6343 my $cur_size = $self->_data_size();
359             # print $tulip "get: $cur_size bytes\n";
360             # close $tulip;
361 3093 100       6599 if($cur_size) {
362 3089         4742 my $rc;
363 3089         4696 eval {
364 3089         10586 $rc = JSON::MaybeXS->new()->ascii()->decode($self->shm()->read($Config{intsize}, $cur_size));
365             };
366 3089 50       203990 if($@) {
367 0         0 $self->_data_size(0);
368 0         0 croak($@);
369             }
370 3089         11075 return $rc;
371             # return JSON::MaybeXS->new()->ascii()->decode($self->shm()->read($Config{intsize}, $cur_size));
372             }
373 4         23 return {};
374             }
375              
376             =head2 BUILD
377              
378             Constructor - validate arguments
379              
380             =cut
381              
382             sub BUILD {
383 99     99 1 523547 my $self = shift;
384              
385 99 100       3179 unless($self->shm_key()) {
386 2         39 croak 'CHI::Driver::SharedMem - no key given';
387             }
388 97         673 $| = 1;
389             }
390              
391             =head2 DEMOLISH
392              
393             If there is no data in the shared memory area, and no-one else is using it,
394             it's safe to remove it and reclaim the memory.
395              
396             =cut
397              
398             sub DEMOLISH {
399             # if(defined($^V) && ($^V ge 'v5.14.0')) {
400             # return if ${^GLOBAL_PHASE} eq 'DESTRUCT'; # >= 5.14.0 only
401             # }
402 99     99 1 105301 my $self = shift;
403              
404             # open(my $tulip, '>>', '/tmp/tulip');
405             # print $tulip "DEMOLISH\n";
406 99 100 66     2908 if($self->shm_key() && $self->shm()) {
407 97         163 my $cur_size;
408 97         281 $self->_lock(type => 'write');
409 97         256 $cur_size = $self->_data_size();
410             # print $tulip "DEMOLISH: $cur_size bytes\n";
411 97         194 my $can_remove = 0;
412 97         2786 my $stat = $self->shm()->stat();
413 97 100       13777 if($cur_size == 0) {
414 2 50 33     42 if(defined($stat) && ($stat->nattch() == 1)) {
415 2         115 $self->shm()->detach();
416 2         140 $self->shm()->remove();
417 2         172 $can_remove = 1;
418             }
419             # } elsif(defined($stat) && ($stat->nattch() == 1)) {
420             # # Scan the cache and see if all has expired.
421             # # If it has, then the cache can be removed if nattch = 1
422             # $can_remove = 1;
423             # foreach my $namespace($self->get_namespaces()) {
424             # print $tulip "DEMOLISH: namespace = $namespace\n";
425             # foreach my $key($self->get_keys($namespace)) {
426             # # May give substr error in CHI
427             # print $tulip "DEMOLISH: key = $key\n";
428             # if($self->is_valid($key)) {
429             # print $tulip "DEMOLISH: is_valid\n";
430             # $can_remove = 0;
431             # last;
432             # }
433             # }
434             # }
435             # $self->shm()->detach();
436             # if($can_remove) {
437             # $self->shm()->remove();
438             # }
439             } else {
440 95         2982 $self->shm()->detach();
441             }
442 97         3939 $self->_unlock();
443 97 50 66     2473 if($can_remove && (my $lock_file = $self->lock_file())) {
444 0           $self->lock_file(undef);
445 0           close $self->lock();
446 0           unlink $lock_file;
447             # print $tulip "unlink $lock_file\n";
448             # close $tulip;
449             }
450             }
451             }
452              
453             =head1 AUTHOR
454              
455             Nigel Horne, C<< <njh at bandsman.co.uk> >>
456              
457             =head1 BUGS
458              
459             Please report any bugs or feature requests to C<bug-chi-driver-sharedmem at rt.cpan.org>, or through
460             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CHI-Driver-SharedMem>. I will be notified, and then you'll
461             automatically be notified of progress on your bug as I make changes.
462              
463             Max_size is handled, but if you're not consistent across the calls to each cache,
464             the results are unpredictable because it's used to create the size of the shared memory
465             area.
466              
467             The shm_size argument should be deprecated and only the max_size argument used.
468              
469             =head1 SEE ALSO
470              
471             L<CHI>, L<IPC::SharedMem>
472              
473             =cut
474              
475             =head1 SUPPORT
476              
477             You can find documentation for this module with the perldoc command.
478              
479             perldoc CHI::Driver::SharedMemory
480              
481             You can also look for information at:
482              
483             =over 4
484              
485             =item * MetaCPAN
486              
487             L<https://metacpan.org/dist/CHI-Driver-SharedMem>
488              
489             =item * RT: CPAN's request tracker
490              
491             L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=CHI-Driver-SharedMemory>
492              
493             =item * CPAN Testers' Matrix
494              
495             L<http://matrix.cpantesters.org/?dist=CHI-Driver-SharedMemory>
496              
497             =item * CPAN Testers Dependencies
498              
499             L<http://deps.cpantesters.org/?module=CHI::Driver::SharedMemory>
500              
501             =back
502              
503             =head1 LICENSE AND COPYRIGHT
504              
505             Copyright 2010-2023 Nigel Horne.
506              
507             This program is released under the following licence: GPL2
508              
509             =cut
510              
511             1;