File Coverage

blib/lib/Cache/RamDisk/Functions.pm
Criterion Covered Total %
statement 84 195 43.0
branch 18 64 28.1
condition 7 28 25.0
subroutine 14 15 93.3
pod 4 4 100.0
total 127 306 41.5


line stmt bran cond sub pod time code
1             =head1 TITLE: Cache::RamDisk::Functions
2              
3             Script-like things for installing and monitoring a Cache::RamDisk
4              
5             =head1 SYNOPSIS
6              
7             cache_install( { 'Base' => '/tmp/rd',
8             'Size' => 16,
9             'INodes' => 1024,
10             'SIndex' => { 'fie' => 8,
11             'foe' => 64,
12             'fum' => 512 },
13             'ShMem' => 'RdLk',
14             'Keys' => { 'fie' => 50,
15             'foe' => 200,
16             'fum' => 4000 },
17             'User' => 'apache',
18             'Group' => 'apache' } );
19              
20              
21             cache_status ('/tmp/rd');
22              
23             cache_remove ('/tmp/rd');
24              
25              
26             =head1 VERSION
27              
28             0.1.6
29              
30             =head1 EXPORTS
31              
32             cache_install
33             cache_status
34             cache_remove
35             cache_objects
36              
37              
38             =head1 REQUIRES
39              
40             Perl B<5.6.1> on a Linux/ Unix System containing the following binaries:
41              
42             chown, mkdir, mke2fs, mount, umount
43              
44             The package uses these Perl modules available from CPAN:
45              
46             IPC::Shareable
47             IPC::SysV
48             Filesys::Df
49             Filesys::Statvfs
50             File::stat
51             Fcntl
52             Symbol
53             Class::Struct
54             POSIX
55              
56              
57             =head1 DESCRIPTION
58              
59             The package provides programmers with functions for creating, monitoring and removing a cache based on a
60             bundle of ramdisks.
61              
62             =head2 cache_install ( $href )
63              
64             Initialize the rd bundle. What will actually happen when you call this method depends on how the system
65             kernel had been compiled. Please refer to the manpages about lilo.conf, lsmod etc. for some
66             further details about manipulating the standard rd size on your box. All rds will be formatted with
67             standard e2fs and under the default blocksize parameter.
68             Of course the calling process has to have root privileges.
69              
70             C does not terminate the calling process after an error has occurred, but emits a warning and
71             returns a somehow valuable result! After successful execution the function calls C on the freshly
72             installed cache and passes the return value to the caller.
73              
74             From version 0.1.5 on C tries to find out whether there an C has been installed. The most effective - but
75             nevertheless hack-like - way to me was to grep in C. If an entry named 'initrd' can be found, the first rd it tries to install
76             will be on /dev/ram1, assuming that /dev/ram0 is occupied in some way...
77              
78              
79             =head3 Arguments
80              
81             Of course all argument names are case sensitive.
82              
83             =head4 Base
84              
85             'Base' is an optional argument defaulting to '/tmp/rd'. Please note that the argument will not be treated
86             as a pathname, but as the beginning of it. With the default value the rds will be called '/tmp/rd0', '/tmp/rd1', ...
87             From version 0.1.5 on can be stated that an initrd being found on '/tmp/rd0' will be respected.
88              
89             =head4 Size
90              
91             The B user space in B to be installed. "Minimum" results from the root space that ext2 reserves on
92             each disk. The creating loop stops when there are more blocks available than necessary for the requested
93             value. This will in effect lead to an available space of ca. 19 MB when you want just 16 - but there aren't
94             many of such opportunities in life, so don't complain.. 'Size' is an optional argument that defaults to 16.
95              
96             =head4 SIndex
97              
98             Each key's index will be stored on a shared memory segment of the key's 'SIndex' Size in B.
99             This is an optional value with a default of 128 for each key. An index element will have the length
100             of the item's id plus 4 bytes (or 5 if there are more than 10 rds keeping the data). In order to speed
101             up performance the indexes are stored as strings and parsed by regexes, which I expect to act somewhat
102             faster than painful C loops over arrays.
103              
104             Although the installation doesn't care about how the cache will be used, setting the 'SIndex' values
105             predefines its policies: when you know that you will always treat a key under LRU aspects with a rather
106             low amount of items to be stored (e.g. C<{'fie' => 50}>), the index will probably be no longer than 2 kB.
107             On the other hand storing sessions with keys of 32 bytes of size each and lifetimes of one hour may lead to
108             an index that doesn't fit into 128 kB.
109              
110              
111             =head4 ShMem
112              
113             All data the install function has gathered so far are static data for a running cache. They consist of
114              
115             =over4
116              
117             =item *
118              
119             the total rds allocated,
120              
121             =item *
122              
123             their common blocksize,
124              
125             =item *
126              
127             the cache keys and their limiting values and keys under which the indexes can be accessed on the shared
128             memory.
129              
130             =back
131              
132             They all have to be stored somewhere, hence another shmem segment came out to be the appropriate place.
133             The value 'ShMem' awaits is the key, under which this segment will be reachable. Please see L
134             for details on what this value is allowed to be.
135              
136             The existence of this argument may look weird to some people, as the shmem key could easily be calculated
137             through C from one of the freshly created directories as well.
138             But implementing it enables your cache to serve more than just one
139             application at the same time - as long as they all stick to the same cache control segment.
140              
141             'ShMem' is an optional argument with a default of 'RdLk'. You will always be advised well when you leave
142             it untouched - unless there is another application running on your server which you can't persuade of
143             using another key. Because if another key than the default is used, it has to passed to every
144             C constructor. ;)
145              
146             =head4 INodes
147              
148             The number of inodes to be reserved for the filesystem on each disk, see L. This optional
149             parameter defaults to 1024. Both the number of inodes and that of the available blocks determine how the
150             disks' capacities will be used by the cache: when you know that most of the objects to be stored will have
151             a rather small size (around 1k) it can make sense to double the value, as else only about 50% of the
152             disk spaces may be occupied. But mostly the default should suffice. Please note that as for the current
153             version it is not intended to let programmers alter the blocksize on the rds. But this may change, and
154             any comments being able to change my mind about this item will be appreciated.
155              
156             =head4 Keys
157              
158             'Keys' is the only mandatory argument, as a cache without a cache key wouldn't make a sense. 'Keys' awaits
159             a hashref, where the keys are the cache's keys, and the values limitate each individual Cache::RamDisk
160             instance's behaviour: for a TIMED instance a value means an item's maximum lifetime in seconds, a LRU
161             instance treats a cache key's value as maximum number of items allowed. Per DEFAULT the cache is ignorant.
162              
163             =head4 User/ Group
164              
165             The system user and group allowed to access the cache. Values have to be real names and not numeric ids.
166             Both arguments are optional and default to 'root'.
167              
168              
169             =head3 What happened?
170              
171             cd /tmp/rd0 && ls -la
172             ipcs
173              
174              
175             =head2 cache_status ( $basename [, $shmemkey] )
176              
177             my $s = cache_status('/tmp/rd');
178             print $s->key_stat('fie');
179             print $s->rd_stat(0)->{bavail};
180              
181             The monitoring tool for a running cache. Requires the cache's base pathname (-fragment, see cache_install), and
182             the 'ShMem' key for this cache, if another than the default value 'RdLk' had been chosen.
183             Always (!) returns a Class::Struct reference with the following accessible members:
184              
185             $s->error # contains the error message in case something went wrong
186             $s->start_disk # the index of the first rd allocated
187             $s->disks # the total of allocated disks
188             $s->blocksize # guess what
189             $s->keys # a key's limit, as set in 'Keys'
190             $s->key_stat # the number of items currently being stored for a key
191             $s->rd_stat # the resulting hashref from a df() call on a rd
192              
193              
194             =head2 cache_objects ( $basename [, $shmemkey] )
195              
196             Monitoring tool #2: get lists of all cached objects. (new in 0.1.6) Returns a hashref keyed to object types. For each object
197             type the value is another hashref, keyed to the rds' numbers and containing arrayrefs with the object ids as values. E.g.:
198              
199             { 'User' => { '1' => [ 2, 5, 67, 8999 ],
200             '2' => [ 1, 3, 4, 66 ]
201             },
202             'Foo' => { '1' => [ 'fie', 'fee', 'fum' ],
203             '2' => [ 'blah', 'bar', 'baz' ]
204             }
205             }
206              
207             =head2 cache_remove ( $basename [, $shmemkey] )
208              
209             Completely clears all devices (by unmounting them) and removes all relevant shared memory segments.
210             Awaits the same arguments as cache_status. Returns 1 on success, else emits a warning and returns 0.
211              
212              
213             =head1 NOTES
214              
215             As both key and internal information are stored on the 'ShMem' segment, they have to be distinguishable
216             from another: internal keys all begin and end with each a double underscore. From this follows that input
217             keys matching the pattern C are ignored by C.
218              
219             The same applies to key names containing any Perl non-word chars (C).
220              
221             =head1 SEE ALSO
222              
223             L, L, L, L, L, L
224              
225              
226             =head1 AUTHOR
227              
228             Martin Haase-Thomas Ethcsoft@snafu.deE
229              
230             =head1 HISTORY
231              
232             B<0.1.6> (08/04/03) Fixed some samll bugs, added C method.
233              
234             B<0.1.5> Some smaller changes due to my newly achieved respect for initrd's.
235              
236             B<0.1.4> Nothing serious. Just beautified the docs a little.
237              
238             B<0.1.3> Implemented 'SIndex' as hashref for assigning shmem sizes to the keys directly.
239              
240             B<0.1.2> dropped the idea of keeping any internal data on the rds and added 'SIndex' arg, rewrote locking
241             again. Added cache_remove.
242              
243             B<0.1.1> rewrote locking concept and cache_install, wrote cache_status. Stress tests showed an
244             extremely lame performance.
245              
246             B<0.1> Jul. 02, cache_install ok, but cache unuseable: locking unclear
247              
248              
249             =head1 TODO
250              
251             What about that funny blocksize story?
252              
253              
254              
255             =cut
256              
257             ##############################################################################
258              
259             package Cache::RamDisk::Functions;
260              
261 1     1   848 use strict;
  1         2  
  1         103  
262 1     1   6 use warnings;
  1         2  
  1         41  
263 1     1   5 no warnings 'untie';
  1         1  
  1         42  
264 1     1   5 use vars qw($VERSION @ISA @EXPORT);
  1         1  
  1         103  
265             $VERSION = 0.1.6;
266             @ISA = qw(Exporter);
267             @EXPORT = qw(cache_install cache_status cache_objects cache_remove);
268              
269 1     1   10 use IPC::SysV 'ftok';
  1         2  
  1         80  
270 1     1   6 use IPC::Shareable qw(:lock);
  1         2  
  1         169  
271 1     1   6 use Filesys::Df;
  1         1  
  1         72  
272 1     1   6 use Filesys::Statvfs;
  1         1  
  1         65  
273 1     1   6 use File::stat;
  1         2  
  1         10  
274 1     1   67 use Symbol 'gensym';
  1         2  
  1         59  
275 1     1   6 use Class::Struct;
  1         2  
  1         8  
276              
277             sub cache_install {
278 1 50   1 1 74 if ($<) {
279 0         0 warn "You must be root to install a cache";
280 0         0 return {}; # a somehow useable value...
281             }
282 1   50     5 my $args = shift || return 0;
283 1         2 my ($i, $rdpath, $hdl, $ret, @stat);
284 1 50       4 $args->{'Base'} = '/tmp/rd' unless $args->{'Base'};
285 1 50       5 $args->{'Size'} = 16 unless $args->{'Size'};
286 1 50       4 $args->{'INodes'}= 1024 unless $args->{'INodes'};
287 1 50       5 $args->{'ShMem'} = 'RdLk' unless $args->{'ShMem'};
288 1 50       4 $args->{'User'} = 'root' unless $args->{'User'};
289 1 50       6 $args->{'Group'} = 'root' unless $args->{'Group'};
290 1 50       4 unless ($args->{'Keys'}) {
291 0         0 warn "A cache like me needs a key";
292 0         0 return {};
293             }
294 1 50       4 $args->{'SIndex'} = {} unless $args->{'SIndex'};
295              
296 1         2 my @keys;
297 1         1 foreach (keys %{$args->{'Keys'}}) {
  1         5  
298 3 50 33     16 unless(/\W/ or /^__.*__$/) {
299 3         3 push @keys, $_;
300 3 50       11 $args->{'SIndex'}->{$_} = 128 unless $args->{'SIndex'}->{$_};
301             }
302              
303             }
304 1         5 $ret = { 'Disks' => 0, 'DStart' => 0, 'Blocks' => 0, 'BSize' => 1024 };
305              
306             # new in 0.1.5: respect an eventual initrd.
307             # to me this looks like an ugly hack...
308 1         7 $hdl = gensym;
309 1         101 open $hdl, '/proc/mounts' || do { warn "Oops! Is this a linux box? Can't open /proc/mounts: $!";
310             return $ret;
311             };
312 1         97 @stat = (<$hdl>);
313 1         12 close $hdl;
314 1 50       9 $ret->{'DStart'} = 1 if (grep 'initrd', @stat);
315              
316 1         8 print STDERR "\n"; # some scripts don't make a nice display... ;)
317 1         3 for ($i=$ret->{'DStart'};;$i++) {
318              
319 1         4 $rdpath = $args->{'Base'}.$i;
320 1         4 $hdl = gensym;
321 1         60 open $hdl, '/etc/mtab' || do { warn "Can't open /etc/mtab: $!";
322             return $ret;
323             };
324 1         58 my @mount = (<$hdl>);
325 1         9 close $hdl;
326 1 50 33     28 if (grep(/$rdpath/, @mount) && system("umount ".$rdpath) < 0) {
327 0         0 warn "Can't unmount $rdpath: $!";
328 0         0 return $ret;
329             }
330 1 50       25 if (-e "/dev/ram$i") {
331 0 0 0     0 if (system("mke2fs -q -N".$args->{'INodes'}." /dev/ram$i") < 0 ||
      0        
332             system("mkdir -p $rdpath") < 0 ||
333             system("mount /dev/ram$i $rdpath")) {
334 0         0 warn "Error while creating /dev/ram$i on $rdpath: $!";
335 0         0 return $ret;
336             }
337 0 0       0 $ret->{'BSize'} = (statvfs $rdpath)[0] unless $i;
338              
339             # chowning must not affect a 'lost+found' directory, that's why it's not done recursively
340             # for the whole of each disk, but lets the '.' directory belong to root
341 0         0 foreach (@keys) {
342 0 0       0 unless (mkdir "$rdpath/$_") {
343 0         0 warn "Error while creating directories on $rdpath: $!";
344 0         0 return $ret;
345             }
346 0 0       0 if (system("chown -R ".$args->{'User'}.".".$args->{'Group'}." $rdpath/$_") < 0) {
347 0         0 warn "Unable to change ownership of $rdpath/$_: $!";
348 0         0 return $ret;
349             }
350             }
351 0         0 @stat = statvfs($rdpath); # df doesn't return the blocksize ?!
352 0         0 $ret->{'Blocks'} += $stat[4];
353 0         0 $ret->{$i} = $stat[0];
354 0         0 $ret->{'Disks'} = $i;
355 0 0       0 last if $ret->{'Blocks'} > $args->{'Size'}*$ret->{'BSize'};
356              
357             }
358             else {
359 1         11 warn "Not enough devices for ".$args->{'Size'}."MB - run 'man MAKEDEV'";
360 1         10 return $ret;
361             }
362             }
363              
364             # write static data to the control segment:
365             # 1. get the shmem keys
366 0         0 my @ftoks;
367 0         0 for ($i = 0; $i < @keys; $i++) {
368 0         0 $ftoks[$i] = ftok($args->{'Base'}.$ret->{'DStart'}."/$keys[$i]", 0);
369             }
370              
371 0         0 my (%cache, $stie);
372 0 0       0 unless (eval { $stie = tie %cache, 'IPC::Shareable', $args->{'ShMem'}, { create => 1, mode => 0666,
  0         0  
373             size => 65536, exclusive => 0,
374             destroy => 0 } } ) {
375 0         0 warn $@;
376 0         0 return $ret;
377             }
378 0         0 $stie->shlock;
379 0         0 $cache{__Disks__} = $ret->{'Disks'};
380 0         0 $cache{__BSize__} = $ret->{'BSize'};
381 0         0 $cache{__DStart__} = $ret->{'DStart'};
382              
383 0         0 for ($i = 0; $i < @keys; $i++) {
384 0         0 $cache{$keys[$i]} = $args->{'Keys'}->{$keys[$i]}.":0:$ftoks[$i]:".$args->{'SIndex'}->{$keys[$i]};
385             }
386             # foreach (keys %cache) { print STDERR "$_=".$cache{$_}."\n";}
387 0         0 $stie->shunlock;
388 0         0 undef $stie;
389 0         0 untie %cache;
390             # finally create the shmem segments and prefill them
391 0         0 for ($i = 0; $i < @keys; $i++) {
392 0         0 my $baz = "";
393 0 0       0 unless (eval { $stie = tie $baz, 'IPC::Shareable', $ftoks[$i], { create => 1, mode => 0666,
  0         0  
394             size => $args->{'SIndex'}->{$keys[$i]}*1024,
395             exclusive => 0, destroy => 0 } } ) {
396 0         0 warn $@;
397 0         0 return $ret;
398             }
399 0         0 $stie->shlock;
400 0         0 $baz = "";
401 0         0 $stie->shunlock;
402 0         0 undef $stie;
403 0         0 untie $baz;
404             }
405              
406 0         0 cache_status ($args->{'Base'}, $args->{'ShMem'});
407             }
408              
409             # monitoring tool
410             sub cache_status {
411 1 50   1 1 20 struct ( c_status => { disks => '$',
412             blocksize => '$',
413             keys => '*%',
414             rd_stat => '*%',
415             key_stat => '*%',
416             error => '$',
417             start_disk => '$'
418             } ) unless defined &c_status::new;
419 1         1498 my $stat = new c_status (disks => 0, blocksize => 1024, error => 0);
420 1   33     64 my $rdpath = shift || do {
421             $stat->error("Argument missing");
422             return $stat;
423             };
424 1         2 my (%cache, $tie);
425 1   50     7 my $shkey = shift || 'RdLk';
426 1 50       2 unless (eval { $tie = tie %cache, 'IPC::Shareable', $shkey, { create => 0, destroy => 0,
  1         8  
427             exclusive => 0, mode => 0666,
428             size => 65536 } } ) {
429 1         411 $stat->error($@);
430 1         9 return $stat;
431             }
432              
433 0         0 $tie->shlock(LOCK_SH);
434              
435             # 1. general:
436 0         0 $stat->disks($cache{__Disks__});
437 0         0 $stat->blocksize($cache{__BSize__});
438 0         0 $stat->start_disk($cache{__DStart__}); # new in 0.1.5
439              
440             # 2. key infos:
441 0         0 my @key;
442 0         0 foreach (keys %cache) {
443 0 0       0 unless (/^__.*__/) {
444 0         0 @key = split /:/, $cache{$_};
445 0         0 $stat->keys($_, $key[0]);
446 0         0 $stat->key_stat($_, $key[1]);
447             }
448             }
449 0         0 $tie->shunlock;
450              
451             # 3. disk infos:
452 0         0 for (my $i = $stat->start_disk; $i < $stat->disks+$stat->start_disk; $i++) {
453 0         0 $stat->rd_stat($i, df($rdpath.$i));
454             }
455              
456 0         0 undef $tie;
457 0         0 untie %cache;
458              
459 0         0 $stat;
460             }
461              
462             # new in 0.1.6: monitoring tool, pt.2:
463             sub cache_objects {
464 0   0 0 1 0 my $rdpath = shift || die "Argument missing!";
465 0         0 my (%cache, $tie);
466 0   0     0 my $shkey = shift || 'RdLk';
467 0 0       0 die $@ unless (eval { $tie = tie %cache, 'IPC::Shareable', $shkey, { create => 0, destroy => 0,
  0         0  
468             exclusive => 0, mode => 0666,
469             size => 65536 } } );
470 0         0 my $res = {};
471 0         0 my (@key, $idx, $xtie, $obj, @tidx, $ikey, $rd, $id);
472 0         0 foreach $ikey (keys %cache) {
473 0         0 @key = split /:/, $cache{$ikey}; # [0] = cache value, [1] = items on cache, [2] = ftok, [3] = shmemsize
474 0 0       0 unless (eval { $xtie = tie $idx, 'IPC::Shareable', $key[2], { create => 0, destroy => 0,
  0         0  
475             exclusive => 0, mode => 0666,
476             size => $key[3]*1024 } } ) {
477 0         0 undef $tie;
478 0         0 untie %cache;
479 0         0 die $@;
480             }
481 0 0       0 unless ($ikey =~ /^__\w+__$/) {
482 0         0 $obj = {};
483 0         0 @tidx = split /\n/, $idx;
484 0         0 foreach (@tidx) {
485 0         0 ($rd, $id) = split /\/\//;
486 0 0       0 $obj->{$rd} = [] unless defined $obj->{$rd};
487 0         0 push @{$obj->{$rd}}, $id;
  0         0  
488             }
489 0         0 $res->{$ikey} = $obj;
490             }
491 0         0 undef $xtie;
492 0         0 untie $idx;
493             }
494              
495 0         0 undef $tie;
496 0         0 untie %cache;
497 0         0 $res;
498             }
499              
500             # remove all system resources related to a cache.
501             sub cache_remove {
502 1 50   1 1 17 if ($<) {
503 0         0 warn "You must be root to remove a cache";
504 0         0 return 0;
505             }
506              
507 1   33     4 my $rdpath = shift || do {
508             warn "Argument missing";
509             return 0;
510             };
511 1         2 my (%cache, $tie);
512 1   50     5 my $shkey = shift || 'RdLk';
513 1 50       2 unless (eval { $tie = tie %cache, 'IPC::Shareable', $shkey, { create => 0, destroy => 0,
  1         7  
514             exclusive => 0, mode => 0666,
515             size => 65536 } } ) {
516 1         348 warn $@;
517 1         5 return 0;
518             }
519 0           $tie->shlock(LOCK_EX);
520              
521 0           for (my $rd = $cache{__DStart__}; $rd < $cache{__Disks__}+$cache{__DStart__}; $rd++) {
522 0           system "umount $rdpath".$rd;
523             }
524              
525 0           foreach (keys %cache) {
526 0 0         next if /^__.*__$/;
527 0           my (@key, $ttie, $idx);
528 0           @key = split /:/, $cache{$_};
529 0 0         unless (eval { $ttie = tie $idx, 'IPC::Shareable', $key[2], { create => 0, destroy => 0,
  0            
530             exclusive => 0, mode => 0666,
531             size => $key[3]*1024 } } ) {
532 0           warn $@;
533 0           return 0;
534             }
535 0           $ttie->remove;
536             }
537 0           $tie->remove;
538 0           1;
539             }
540              
541             1;