File Coverage

blib/lib/CHI/Driver/SharedMem.pm
Criterion Covered Total %
statement 153 170 90.0
branch 35 50 70.0
condition 8 24 33.3
subroutine 27 27 100.0
pod 10 10 100.0
total 233 281 82.9


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   453118 use warnings;
  5         25  
  5         202  
8 5     5   55 use strict;
  5         11  
  5         134  
9 5     5   1873 use CHI::Constants qw(CHI_Meta_Namespace);
  5         2013  
  5         257  
10 5     5   2922 use Moose;
  5         2467247  
  5         33  
11 5     5   42169 use IPC::SysV qw(S_IRUSR S_IWUSR IPC_CREAT);
  5         4346  
  5         503  
12 5     5   1103 use IPC::SharedMem;
  5         10378  
  5         163  
13 5     5   561 use JSON::MaybeXS;
  5         6337  
  5         392  
14 5     5   36 use Carp;
  5         23  
  5         298  
15 5     5   71 use Config;
  5         14  
  5         212  
16 5     5   36 use Fcntl;
  5         10  
  5         11909  
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_file' => (is => 'rw', isa => 'Str|Undef');
24             has 'lock_fd' => (
25             is => 'ro',
26             builder => '_build_lock',
27             );
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.19
51              
52             =cut
53              
54             our $VERSION = '0.19';
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 308339 my($self, $key, $value) = @_;
108              
109 548         1574 $self->_lock(type => 'write');
110 548         2130 my $h = $self->_data();
111 548         2483 $h->{$self->namespace()}->{$key} = $value;
112             # if($self->{'is_size_aware'}) {
113             # $h->{CHI_Meta_Namespace()}->{'last_used_time'}->{$key} = time;
114             # }
115 548         1504 $self->_data($h);
116 547         1346 $self->_unlock();
117             }
118              
119             =head2 fetch
120              
121             Retrieves an object from the cache
122              
123             =cut
124              
125             sub fetch {
126 1783     1783 1 747399 my($self, $key) = @_;
127              
128 1783 100       4179 if($self->{is_size_aware}) {
129 367         867 $self->_lock(type => 'write');
130             } else {
131 1416         2923 $self->_lock(type => 'read');
132             }
133             # open(my $tulip, '>>', '/tmp/tulip');
134             # print $tulip __LINE__, "\n";
135             # print $tulip __LINE__, "\n";
136 1783         7251 my $rc = $self->_data()->{$self->namespace()}->{$key};
137             # print $tulip __LINE__, "\n";
138 1783 100       11140 if($self->{is_size_aware}) {
139 367         915 my $h = $self->_data();
140 367         950 $h->{CHI_Meta_Namespace()}->{last_used_time}->{$key} = time;
141 367         794 $self->_data($h);
142             }
143 1783         4659 $self->_unlock();
144             # print $tulip __LINE__, "\n";
145             # close $tulip;
146 1783         8948 return $rc;
147             }
148              
149             =head2 remove
150              
151             Remove an object from the cache
152              
153             =cut
154              
155             sub remove {
156 132     132 1 157181 my($self, $key) = @_;
157              
158 132         438 $self->_lock(type => 'write');
159 132 0 33     648 if($ENV{'AUTHOR_TESTING'} && $self->{'is_size_aware'} && (my $timeout = $self->discard_timeout())) {
      0        
160             # Workaround for test_discard_timeout
161             # my $sleep_time = $timeout + 1;
162             # open(my $tulip, '>>', '/tmp/tulip');
163             # print $tulip "sleeping $sleep_time\n";
164             # close $tulip;
165             # sleep($sleep_time);
166 0         0 sleep(1);
167             }
168 132         356 my $h = $self->_data();
169 132         578 delete $h->{$self->namespace()}->{$key};
170 132         380 delete $h->{CHI_Meta_Namespace()}->{last_used_time}->{$key};
171 132         363 $self->_data($h);
172 132         329 $self->_unlock();
173              
174             # open(my $tulip, '>>', '/tmp/tulip');
175             # print $tulip "remove: $key\n";
176             }
177              
178             =head2 clear
179              
180             Removes all data from the current namespace
181              
182             =cut
183              
184             sub clear {
185 109     109 1 66642 my $self = shift;
186              
187 109         392 $self->_lock(type => 'write');
188 109         565 my $h = $self->_data();
189 109         622 delete $h->{$self->namespace()};
190 109         334 $self->_data($h);
191 109         319 $self->_unlock();
192              
193             # open(my $tulip, '>>', '/tmp/tulip');
194             # print $tulip "clear ", $self->namespace(), "\n";
195             }
196              
197             =head2 get_keys
198              
199             Gets a list of the keys in the current namespace
200              
201             =cut
202              
203             sub get_keys {
204 126     126 1 58585 my $self = shift;
205              
206 126         386 $self->_lock(type => 'read');
207 126         540 my $h = $self->_data();
208 126         446 $self->_unlock();
209 126         348 return(keys(%{$h->{$self->namespace()}}));
  126         2153  
210             }
211              
212             =head2 get_namespaces
213              
214             Gets a list of the namespaces in the cache
215              
216             =cut
217              
218             sub get_namespaces {
219 12     12 1 10360 my $self = shift;
220              
221 12         45 $self->_lock(type => 'read');
222 12         56 my $rc = $self->_data();
223 12         46 $self->_unlock();
224             # Needs to be sorted for RT89892
225 12         39 my @rc = sort keys(%{$rc});
  12         86  
226 12         133 return @rc;
227             }
228              
229             =head2 default_discard_policy
230              
231             Use an LRU algorithm to discard items when the cache can't add anything
232              
233             =cut
234              
235 4     4 1 361 sub default_discard_policy { 'lru' }
236              
237             =head2 discard_policy_lru
238              
239             When the Shared memory area is getting close to full, discard the least recently used objects
240              
241             =cut
242              
243             sub discard_policy_lru {
244 14     14 1 932 my $self = shift;
245              
246 14 0 33     52 if($ENV{'AUTHOR_TESTING'} && $self->{'is_size_aware'} && (my $timeout = $self->discard_timeout())) {
      0        
247             # Workaround for test_discard_timeout
248             # my $sleep_time = $timeout + 1;
249             # open(my $tulip, '>>', '/tmp/tulip');
250             # print $tulip "sleeping $sleep_time\n";
251             # close $tulip;
252             # sleep($sleep_time);
253 0         0 sleep(1);
254             }
255 14         51 $self->_lock(type => 'read');
256 14         63 my $last_used_time = $self->_data()->{CHI_Meta_Namespace()}->{last_used_time};
257 14         118 $self->_unlock();
258             my @keys_in_lru_order =
259 14         94 sort { $last_used_time->{$a} <=> $last_used_time->{$b} } $self->get_keys();
  193         324  
260             return sub {
261 36     36   3989 shift(@keys_in_lru_order);
262 14         129 };
263             }
264              
265             # Internal routines
266              
267             # The area must be locked by the caller
268             sub _build_shm {
269 97     97   186 my $self = shift;
270 97         2840 my $shm_size = $self->shm_size();
271              
272 97 50 33     576 if((!defined($shm_size)) || ($shm_size == 0)) {
273             # Probably some strange condition in cleanup
274             # croak 'Size == 0';
275 0         0 return;
276             }
277 97         2706 my $shm = IPC::SharedMem->new($self->shm_key(), $shm_size, S_IRUSR|S_IWUSR);
278 97 100       3101 unless($shm) {
279 4         139 $shm = IPC::SharedMem->new($self->shm_key(), $shm_size, S_IRUSR|S_IWUSR|IPC_CREAT);
280 4 50       516 unless($shm) {
281 0         0 croak "Couldn't create a shared memory area of $shm_size bytes with key ",
282             $self->shm_key(), ": $!";
283 0         0 return;
284             }
285 4         35 $shm->write(pack('I', 0), 0, $Config{intsize});
286             }
287 97         739 $shm->attach();
288 97         7120 return $shm;
289             }
290              
291             sub _build_lock {
292 99     99   557587 my $self = shift;
293              
294             # open(my $fd, '<', $0) || croak("$0: $!");
295             # FIXME: make it unique for each object, not a singleton
296 99         3076 $self->lock_file('/tmp/' . __PACKAGE__);
297             # open(my $tulip, '>>', '/tmp/tulip');
298             # print $tulip "build_lock\n", $self->lock_file(), "\n";
299 99 50       3187 open(my $fd, '>', $self->lock_file()) || croak($self->lock_file(), ": $!");
300             # close $tulip;
301 99         735 return $fd;
302             }
303              
304             sub _lock {
305 2821     2821   7712 my ($self, %params) = @_;
306              
307             # open(my $tulip, '>>', '/tmp/tulip');
308             # print $tulip $params{'type'}, ' lock ', $self->lock_file(), "\n";
309             # my $i = 0;
310             # while((my @call_details = (caller($i++)))) {
311             # print $tulip "\t", $call_details[1], ':', $call_details[2], ' in function ', $call_details[3], "\n";
312             # }
313 2821 50       91938 return unless $self->lock_file();
314              
315 2821 50       77415 if(my $lock = $self->lock_fd()) {
316             # print $tulip "locking\n";
317 2821 100       33662 flock($lock, ($params{type} eq 'read') ? Fcntl::LOCK_SH : Fcntl::LOCK_EX);
318             } else {
319             # print $tulip 'lost lock ', $self->lock_file(), "\n";
320 0         0 croak('Lost lock: ', $self->lock_file());
321             }
322             # print $tulip "locked\n";
323             # close $tulip;
324             }
325              
326             sub _unlock {
327 2821     2821   4257 my $self = shift;
328              
329             # open(my $tulip, '>>', '/tmp/tulip');
330             # print $tulip 'unlock ', $self->lock_file(), "\n";
331             # my $i = 0;
332             # while((my @call_details = (caller($i++)))) {
333             # print $tulip "\t", $call_details[1], ':', $call_details[2], ' in function ', $call_details[3], "\n";
334             # }
335 2821 50       86701 if(my $lock = $self->lock_fd()) {
336 2821         36665 flock($lock, Fcntl::LOCK_UN);
337             } else {
338             # print $tulip 'lost lock for unlock ', $self->lock_file(), "\n";
339 0         0 croak('Lost lock for unlock: ', $self->lock_file());
340             }
341             # close $tulip;
342             }
343              
344             # The area must be locked by the caller
345             sub _data_size {
346 4343     4343   7171 my($self, $value) = @_;
347              
348 4343 50       132148 if(!$self->shm()) {
349 0         0 croak __PACKAGE__, ': panic: _data_size has lost the shared memory segment';
350 0         0 return 0;
351             }
352 4343 100       9492 if(defined($value)) {
353 1155         30629 $self->shm()->write(pack('I', $value), 0, $Config{intsize});
354 1155         19490 return $value;
355             }
356 3188         84624 my $size = $self->shm()->read(0, $Config{intsize});
357 3188 50       75537 unless(defined($size)) {
358 0         0 return 0;
359             }
360 3188         9805 return unpack('I', $size);
361             }
362              
363             # The area must be locked by the caller
364             sub _data {
365 4247     4247   8813 my($self, $h) = @_;
366              
367             # open(my $tulip, '>>', '/tmp/tulip');
368             # print $tulip __LINE__, "\n";
369 4247 100       9330 if(defined($h)) {
370 1156         3411 my $f = JSON::MaybeXS->new()->ascii(1)->encode($h);
371 1156         55958 my $cur_size = length($f);
372             # print $tulip __LINE__, " cmp $cur_size > ", $self->shm_size(), "\n";
373 1156 100       39622 if($cur_size > ($self->shm_size() - $Config{intsize})) {
374 1         5 $self->_unlock();
375 1         42 croak("Sharedmem set failed - value too large? ($cur_size bytes) > ", $self->shm_size());
376             }
377 1155 50       5712 if($f !~ /\}$/) {
378 0         0 $self->_unlock();
379 0         0 croak("Encoding failed. ($cur_size bytes: $f) ");
380             }
381 1155         31859 $self->shm()->write($f, $Config{intsize}, $cur_size);
382 1155         22681 $self->_data_size($cur_size);
383             # print $tulip "set: $cur_size bytes\n";
384             # close $tulip;
385 1155         4427 return $h;
386             }
387 3091         6868 my $cur_size = $self->_data_size();
388             # print $tulip "get: $cur_size bytes\n";
389 3091 100       6970 if($cur_size) {
390 3087         4306 my $rc;
391 3087         4846 eval {
392 3087         10971 $rc = JSON::MaybeXS->new()->ascii(1)->decode($self->shm()->read($Config{intsize}, $cur_size));
393             };
394 3087 50       211816 if($@) {
395 0         0 $self->_lock(type => 'write');
396 0         0 $self->_data_size(0);
397 0         0 my $foo = $self->shm()->read($Config{intsize}, $cur_size);
398             # print $tulip "\tDecode fail $cur_size bytes $@\n\t$foo\n";
399             # my $i = 0;
400             # while((my @call_details = (caller($i++)))) {
401             # print $tulip "\t", $call_details[1], ':', $call_details[2], ' in function ', $call_details[3], "\n";
402             # }
403 0         0 croak($@);
404 0         0 $self->_unlock();
405             }
406 3087         12179 return $rc;
407             # return JSON::MaybeXS->new()->ascii(1)->decode($self->shm()->read($Config{intsize}, $cur_size));
408             }
409             # close $tulip;
410 4         28 return {};
411             }
412              
413             =head2 BUILD
414              
415             Constructor - validate arguments
416              
417             =cut
418              
419             sub BUILD {
420 99     99 1 506673 my $self = shift;
421              
422 99 100       3178 unless($self->shm_key()) {
423 2         54 croak 'CHI::Driver::SharedMem - no shm_key given';
424             }
425 97         735 $| = 1;
426             }
427              
428             =head2 DEMOLISH
429              
430             If there is no data in the shared memory area, and no-one else is using it,
431             it's safe to remove it and reclaim the memory.
432              
433             =cut
434              
435             sub DEMOLISH {
436             # if(defined($^V) && ($^V ge 'v5.14.0')) {
437             # return if ${^GLOBAL_PHASE} eq 'DESTRUCT'; # >= 5.14.0 only
438             # }
439 99     99 1 105469 my $self = shift;
440              
441             # open(my $tulip, '>>', '/tmp/tulip');
442             # print $tulip "DEMOLISH\n";
443 99 100 66     2898 if($self->shm_key() && $self->shm()) {
444 97         307 $self->_lock(type => 'write');
445 97         405 my $cur_size = $self->_data_size();
446             # print $tulip "DEMOLISH: $cur_size bytes\n";
447 97         174 my $can_remove = 0;
448 97         2761 my $stat = $self->shm()->stat();
449 97 100       14127 if($cur_size == 0) {
450 2 50 33     64 if(defined($stat) && ($stat->nattch() == 1)) {
451 2         86 $self->shm()->detach();
452 2         151 $self->shm()->remove();
453 2         188 $can_remove = 1;
454             }
455             # } elsif(defined($stat) && ($stat->nattch() == 1)) {
456             # # Scan the cache and see if all has expired.
457             # # If it has, then the cache can be removed if nattch = 1
458             # $can_remove = 1;
459             # foreach my $namespace($self->get_namespaces()) {
460             # print $tulip "DEMOLISH: namespace = $namespace\n";
461             # foreach my $key($self->get_keys($namespace)) {
462             # # May give substr error in CHI
463             # print $tulip "DEMOLISH: key = $key\n";
464             # if($self->is_valid($key)) {
465             # print $tulip "DEMOLISH: is_valid\n";
466             # $can_remove = 0;
467             # last;
468             # }
469             # }
470             # }
471             # $self->shm()->detach();
472             # if($can_remove) {
473             # $self->shm()->remove();
474             # }
475             } else {
476 95         2943 $self->shm()->detach();
477             }
478 97         4108 $self->_unlock();
479 97 100 66     2425 if($can_remove && (my $lock_file = $self->lock_file())) {
480 2         58 $self->lock_file(undef);
481 2         71 close $self->lock_fd();
482 2         197 unlink $lock_file;
483             # print $tulip "unlink $lock_file\n";
484             # close $tulip;
485             }
486             }
487             }
488              
489             =head1 AUTHOR
490              
491             Nigel Horne, C<< <njh at bandsman.co.uk> >>
492              
493             =head1 BUGS
494              
495             Please report any bugs or feature requests to C<bug-chi-driver-sharedmem at rt.cpan.org>, or through
496             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CHI-Driver-SharedMem>. I will be notified, and then you'll
497             automatically be notified of progress on your bug as I make changes.
498              
499             Max_size is handled, but if you're not consistent across the calls to each cache,
500             the results are unpredictable because it's used to create the size of the shared memory
501             area.
502              
503             The shm_size argument should be deprecated and only the max_size argument used.
504              
505             =head1 SEE ALSO
506              
507             L<CHI>, L<IPC::SharedMem>
508              
509             =cut
510              
511             =head1 SUPPORT
512              
513             You can find documentation for this module with the perldoc command.
514              
515             perldoc CHI::Driver::SharedMemory
516              
517             You can also look for information at:
518              
519             =over 4
520              
521             =item * MetaCPAN
522              
523             L<https://metacpan.org/dist/CHI-Driver-SharedMem>
524              
525             =item * RT: CPAN's request tracker
526              
527             L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=CHI-Driver-SharedMemory>
528              
529             =item * CPAN Testers' Matrix
530              
531             L<http://matrix.cpantesters.org/?dist=CHI-Driver-SharedMemory>
532              
533             =item * CPAN Testers Dependencies
534              
535             L<http://deps.cpantesters.org/?module=CHI::Driver::SharedMemory>
536              
537             =back
538              
539             =head1 LICENSE AND COPYRIGHT
540              
541             Copyright 2010-2023 Nigel Horne.
542              
543             This program is released under the following licence: GPL2
544              
545             =cut
546              
547             1;