#-*- perl -*- # # Copyright (C) 2004,2005,2006 Ken'ichi Fukamachi # All rights reserved. This program is free software; you can # redistribute it and/or modify it under the same terms as Perl itself. # # $FML: Ring.pm,v 1.7 2006/10/11 14:54:49 fukachan Exp $ # package FML::Cache::Ring; use strict; use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD); use Carp; use IO::File; use File::Spec; =head1 NAME FML::Cache::Ring - IO operations to ring buffer which consists of files. =head1 SYNOPSIS ... lock e.g. by flock(2) ... use FML::Cache::Ring; $obj = new FML::Cache::Ring { directory => '/some/where' # mandatory sequence_file_name => '.seq', # optional modulus => 128, # optional }; $fh = $obj->open; print $fh "some message"; $fh->close; ... unlock ... The buffer directory has files with the numeric name C<0>, C<1>, ... You can specify C parameter. $obj = new FML::Cache::Ring { directory => '/some/where', file_name => '_smtplog', }; If specified, the file names become _smtplog.0, _smtplog.1, ... The cache data is limited by the number of files, so approximately size by default. Instead of number fo files, you can limit FML::Cache::Ring based on time. It is time based expiretion. To specify based expiration, use new() like this: $obj = new FML::Cache::Ring { directory => '/some/where', cache_type => 'temporal', expires_in => 90, # 90 days }; where C is C by default. =head1 DESCRIPTION To log messages up to some limit, it may be useful to use filenames in cyclic way. The file to write is chosen among a set of files allocated as a buffer. Consider several files under a directory C as a ring buffer where the unit of the ring is 5 here. C may have 5 files in it. 0 1 2 3 4 To log a message is to write it to one of them. At the first time the message is logged to the file C<0>, and next time to C<1> and so on. If all 5 files are used, this module reuses and overwrites the oldest one C<0>. So we use a file in cyclic way as follows: 0 -> 1 -> 2 -> 3 -> 4 -> 0 -> 1 -> ... We expire the old data. A file name is a number for simplicity. The latest number is holded in C file (C<.seq> in that direcotry by default) and truncated to 0 by the modulo C<5>. =head1 METHODS =head2 new(args) $args hash can take the following arguments: variable default value -------------------------------- directory . file_name "" sequence_file_name .seq modulus 128 cache_type cyclic dir_mode 0755 =cut @ISA = qw(IO::File); BEGIN {} END {} # Descriptions: constructor. # forward new() request to superclass (IO::File). # Arguments: OBJ($self) HASH_REF($args) # Side Effects: none # Return Value: OBJ (blessed as a file handle). sub new { my ($self, $args) = @_; my ($type) = ref($self) || $self; my $me = {}; _take_file_name($me, $args); return bless $me, $type; } # Descriptions: determine the file name to write into. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: increment $sequence_file_name # set the file name at ${*$self}{ _file } # Return Value: none sub _take_file_name { my ($self, $args) = @_; my $sequence_file_name = $args->{ sequence_file_name } || '.seq'; my $directory = $args->{ directory } || '.'; my $filename_prefix = $args->{ file_name } || ''; my $modulus = $args->{ modulus } || 128; my $cache_type = $args->{ cache_type } || 'cyclic'; my $dir_mode = $args->{ dir_mode } || 0755; my $file; unless (-d $directory) { use File::Path; mkpath( [ $directory ], 0, $dir_mode); } if ($cache_type eq 'temporal') { my ($sec,$min,$hour,$mday,$mon,$year,$wday) = localtime(time); my $filename = sprintf("%04d%02d%02d", 1900+$year, $mon+1, $mday); $file = File::Spec->catfile($directory, $filename); } elsif ($cache_type eq 'cyclic') { my $seq_file = File::Spec->catfile($directory, $sequence_file_name); use IO::Adapter; my $io = new IO::Adapter $seq_file; my $id = $io->sequence_increment(); # the sequence $id is already incremetd. my $saved_id = $id; # check if $id is rolled over or not. $id = $id % $modulus; if ($saved_id != $id) { $io->sequence_replace($id); } # file. $file = File::Spec->catfile($directory, sprintf("%s%s", $filename_prefix, $id)); } $self->{ _cache_type } = $cache_type || 'cyclic'; $self->{ _cache_data } = {}; $self->{ _directory } = $directory || ''; $self->{ _file } = $file || ''; } # Descriptions: return the path of file to be written. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub cache_file_path { my ($self) = @_; return( $self->{ _opened_file } || ''); } =head2 open(file, mode) open file in the buffer. The target file is already determined when our constructor runs. =cut # Descriptions: open() cache file in the buffer. # Arguments: OBJ($self) STR($file) STR($mode) # XXX $self is blessed file handle. # Side Effects: create ${ *$self } hash to save status information. # Return Value: HANDLE(write file handle for $file.new.$$) sub open { my ($self, $file, $mode) = @_; $file = defined $file ? $file : $self->{ _file }; $mode = defined $mode ? $mode : ($self->{ _cache_type } eq 'temporal' ? "a+" : "w+"); # error return undef unless $file; # If the cache is limited by "time", we only add values to the file. # If limited by space, we ovewrite the file, so open it by the mode "w". my $fh = new IO::File; # real open with $mode if (defined $fh) { $self->{ _opened_file } = $file; $fh->open($file, $mode); $fh->autoflush(1); $self->{ _fh } = $fh; return $fh; } else { return undef; } } =head2 close() no argument. =cut # Descriptions: forward close() to SUPER class # Arguments: OBJ($self) # Side Effects: none # Return Value: same as SUPER::close() or UNDEF sub close { my ($self) = @_; my $fh = $self->{ _fh }; defined $fh ? $fh->close() : undef; } =head2 add($args) import data into cache from file. Actually, link(2) $src to cache file. KEY VALUE --------------------------- file STR try_link STR ( yes | no ) =cut # Descriptions: import data from file. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: create $dst file (linked). # Return Value: NUM(1(success) or 0(fail)) sub add { my ($self, $args) = @_; my $link = $args->{ try_link } || "yes"; my $src = $args->{ file } || ''; my $dst = $self->cache_file_path() || ''; unless ($src) { return 0;} unless ($dst) { $self->open(); $dst = $self->cache_file_path() || ''; } if ($dst && $src) { return 0 unless -f $src; unlink $dst if -f $dst; if (link($src, $dst)) { return 1; } else { return 0; } } return 0; } =head2 get(key) get value (latest value in the ring buffer) for the key. =cut # Descriptions: get value (latest value in the ring buffer) for the key. # Arguments: OBJ($self) STR($key) # Side Effects: none # Return Value: STR sub get { my ($self, $key) = @_; $self->get_latest_value($key); } # Descriptions: get value (latest value in the ring buffer) for the # specified key. # Arguments: OBJ($self) STR($key) # Side Effects: none # Return Value: STR sub get_latest_value { my ($self, $key) = @_; # cheap sanity. return '' unless defined $key; # 1. return matched value if found in the latest cache. my $file = $self->{ _file }; my $buf = $self->_search($file, $key); return $buf if $buf; # 2. if not found in the latest cache, search cacheses in the # cache directory in reverse temporal order. my $dir = $self->{ _directory }; return '' unless $dir; my $dh = new IO::Handle; opendir($dh, $dir); my @dh = (); for my $entry (readdir($dh)) { push(@dh, $entry) if $entry =~ /^\d+/o;} @dh = sort { $b <=> $a } @dh; DIR_ENTRY: for my $_dirent (@dh) { next DIR_ENTRY if $_dirent =~ /^\./o; next DIR_ENTRY if $_dirent !~ /^\d/o; # XXX-TODO: rule ignoring /^\d{1,2}$/ is correct ? next DIR_ENTRY if $_dirent =~ /^\d{1,2}$/; $file = File::Spec->catfile($dir, $_dirent); $buf = $self->_search($file, $key); last DIR_ENTRY if $buf; } closedir($dh) if defined $dh; return $buf; } # Descriptions: search value for $key in the $file. # Arguments: OBJ($self) STR($file) STR($key) # Side Effects: none # Return Value: STR sub _search { my ($self, $file, $key) = @_; my $hash = $self->{ _cache_data }; my $pkey = quotemeta( substr($key, 0, 1) ); my $buf = ''; # simple check return '' unless defined $file; return '' unless $file; # XXX-TODO: negative cache is needed ? # XXX-TODO: when negative cache is expired ? this code is correct ? return '' if defined $hash->{ $file }; $hash->{ $file } = 1; my $fh = $self->open($file, "r"); if (defined $fh) { my $x; ENTRY: while ($x = $fh->getline) { next ENTRY unless $x =~ /^$pkey/; if ($x =~ /^$key\s+/ || $x =~ /^$key$/) { chomp $x; my ($k, $v) = split(/\s+/, $x, 2); $buf = $v; } } $self->close; } return $buf; } =head2 find(key) get value (latest value in the ring buffer) for key. same as get() now. =cut # Descriptions: get value (latest value in the ring buffer) for key. # Arguments: OBJ($self) STR($key) # Side Effects: none # Return Value: STR sub find { my ($self, $key) = @_; $self->get($key); } =head2 set(key, value) set value for key. =cut # Descriptions: set value for key. # Arguments: OBJ($self) STR($key) STR($value) # Side Effects: none # Return Value: same as close() sub set { my ($self, $key, $value) = @_; my $fh = $self->open; if (defined $fh) { printf $fh "%s\t%s\n", $key, $value; $self->close; } } # # debug # if ($0 eq __FILE__) { my $tmp_dir = "/tmp/cachedir"; unless (-d $tmp_dir) { eval q{ use File::Path; mkpath( [ $tmp_dir ], 0, 0755); }; } my $cache = new FML::Cache::Ring { directory => $tmp_dir }; $cache->set(time, time); print STDERR "see $tmp_dir\n"; } =head1 TODO Export core parts of this module to another putlic class e.g. File::SOMETHING outside FML::* classes, again. =head1 CODING STYLE See C on fml coding style guide. =head1 AUTHOR Ken'ichi Fukamachi =head1 COPYRIGHT Copyright (C) 2004,2005,2006 Ken'ichi Fukamachi All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 HISTORY FML::Cache::Ring first appeared in fml8 mailing list driver package. See C for more details. FML::Cache::Ring is renamed from File::CacheDir in 2004. =cut 1;