Cache-Static
view release on metacpan or search on metacpan
lib/Cache/Static.pm view on Meta::CPAN
eval "require $ext;";
next if($@);
my $util = $ext;
$util =~ s/\:\:/_/g;
eval "require Cache::Static::${util}_Util";
if($@) {
_log(2, "$ext exists but Cache::Static::${util}_Util does not, disabling extension\n");
} else {
push @timestamp_extensions, $ext;
}
}
sub _readconf {
my $ns = shift;
$ns = '' unless(defined($ns));
_die_if_invalid_namespace($ns) if($ns);
my $dir = "$ROOT/$ns";
my @conf;
open(CONF, "$dir/config") &&
(@conf = map { my $t = $_; $t = lc($t); $t =~ s/^\s+//; $t =~ s/\s+$//;
my $ar = []; @$ar = split(/\s+/, $t, 2); $ar }
grep(/^[^#]/, grep(/./, <CONF>)));
close(CONF);
foreach my $cr (@conf) {
if($cr->[0] eq 'log_level') {
if(!$ns || $ns eq 'DEFAULT') {
$CONF{log_level} = $cr->[1];
} else {
_log(3, "log_level directive in CONF($ns) ignored");
}
} else {
$CONF{$ns ? $ns : 'DEFAULT'}->{$cr->[0]} = $cr->[1];
}
}
}
#### useful when adding new modules
#warn "time: @timestamp_extensions\n";
#warn "help: @helper_extensions\n";
#die;
sub _has_timestamp {
my $mod = shift;
return grep(/^$mod$/, @timestamp_extensions);
}
sub _has_helper {
my $mod = shift;
return grep(/^$mod$/, @helper_extensions);
}
############################
### /glue for extensions ###
############################
#try to set up the logfile with lenient permissions
eval {
open(FH, ">>$LOGFILE");
close(FH);
chmod 0666, $LOGFILE;
};
#number of levels of directory in cache
#TODO: move this to config file
my $CACHE_LEVELS = 3;
sub get_if_same {
### uncomment the below line to disable Cache::Static
# return undef;
my ($key, $depsref, %args) = @_;
my ($ret, $dep) = _is_same($key, $depsref, %args);
if($ret) {
_log(1, "cache hit for key: $key");
return _get($key, %args);
} else {
_log(1, "cache miss for key: $key on dep: $dep");
return undef;
}
}
sub _die_if_invalid_namespace {
my $ns = shift;
die "illegal namespace: $namespace" if($namespace =~ /\// ||
grep (/^$namespace$/, @ILLEGAL_NAMESPACES));
}
sub set {
my ($key, $content, $deps, %args) = @_;
my $ns = $args{namespace} || $namespace;
_die_if_invalid_namespace($ns);
eval {
#create any necessary directories
my $dir = $key;
$dir =~ s/\/[^\/]*$//;
_mkdir_p("$ROOT/$ns/cache/$dir");
die "couldn't make/walk directories: $@" if($@);
#if we overrode the namespace, or if the dir got rm -rf'd out
#from under us, this comes in handy...
_mkdir_p("$ROOT/$ns/tmp");
#write out the content
my $tmpf = $key;
$tmpf =~ s/\///g;
open(FH, ">$ROOT/$ns/tmp/$tmpf") || die "couldn't open $ROOT/$ns/tmp/$tmpf: $!";
(print FH $content) || die "couldn't print: $!";
close(FH) || die "couldn't close: $!";
chmod 0666, "$ROOT/$ns/tmp/$tmpf";
#move the new cache file in place
(rename "$ROOT/$ns/tmp/$tmpf", "$ROOT/$ns/cache/$key") ||
die "couldn't rename content to $ROOT/$ns/cache/$key";
if($deps) {
#write out the deps
my $frozen_deps = join('', map { $a=$_; $a.="\n"; $a } @$deps);
open(FH, ">$ROOT/$ns/tmp/$tmpf.dep") || die "couldn't open: $!";
(print FH $frozen_deps) || die "couldn't print: $!";
close(FH) || die "couldn't close: $!";
chmod 0666, "$ROOT/$ns/tmp/$tmpf.dep";
#move the new .dep file in place
(rename "$ROOT/$ns/tmp/$tmpf.dep", "$ROOT/$ns/cache/$key.dep") ||
die "couldn't rename deps to $ROOT/$ns/cache/$key.dep: $!";
}
}; if($@) {
_log(2, "Cache::Static::set couldn't save new value (in namespace: $ns) : $@");
} else {
_log(3, "Cache::Static::set refreshed $key in namespace: $ns");
}
}
sub make_friendly_key {
my ($url, $argsref) = @_;
#key for Cache is url + args in deterministic order
my $key = "$url?";
foreach my $arg (sort keys %$argsref) {
my $val = $argsref->{$arg};
if(ref($val)) {
if(ref($val) eq 'ARRAY') {
$val = join("&$arg=", @$val);
} elsif($val->isa('XML::Comma::Doc')
&& _has_timestamp('XML::Comma')) {
$val = "XML::Comma::Doc:".$val->doc_key;
} else {
_log(3, "got a ".ref($val)." and we're just freezing it...");
$val = Storable::freeze($val);
}
}
$key .= "$arg=$val&";
}
$key =~ s/&$//;
#fix problem with friendly keys that have a multiple consecutive dashes,
#as when they are printed in HTML debugging mode, they can cause SGML
#comments to eat what is supposed to be code up to the next literal --
#for one-to-one-ness, also map '-' (single dash) to '-1-'
#this is really something browsers should work around, but don't. see:
# https://bugzilla.mozilla.org/show_bug.cgi?id=214476
$key = join("", map { (/-+/) ? "-".length($_)."-" : $_ }
split(/(-+)/, $key));
return $key;
}
sub make_key {
return md5_path(make_friendly_key(@_));
}
sub make_key_from_friendly {
my $key = shift;
return md5_path($key);
}
sub md5_path {
my $key = shift;
$key = md5_base64($key);
lib/Cache/Static.pm view on Meta::CPAN
}
sub _write_spec_timestamp {
my $spec = shift;
_mkdirs_and_touch($ROOT.'/timestamps/'.md5_path($spec).'.ts', $spec);
}
sub _unlink_spec_timestamp {
my $spec = shift;
my $file = $ROOT.'/timestamps/'.md5_path($spec).'.ts';
unlink($file);
if(_get_conf($namespace, 'recursive_unlink')) {
$file =~ s/\/[^\/]*$//;
unless(opendir(DIR, $file)) {
_log(3, "_unlink_spec_timestamp failed to opendir($file): (another process probably rmdir'd it): $!");
return;
}
my @files = readdir(DIR);
closedir(DIR) if(@files);
while($#files == 1 ) {
unless(rmdir $file) {
_log(3, "_unlink_spec_timestamp failed to rmdir($file): (another process probably touched a file in it): $!");
return;
}
$file =~ s/\/[^\/]*$//;
unless(opendir(DIR, $file)) {
_log(3, "_unlink_spec_timestamp failed to opendir($file): (another process probably rmdir'd it): $!");
return;
}
my @files = readdir(DIR);
closedir(DIR) if(@files);
}
}
}
#optional second argument indicates stuff to squirrel in the file
#TODO: the name is misleading given the possibility of the 2nd arg
sub _mkdirs_and_touch {
my $file = shift;
my $output = shift || '';
#get rid of double slashes
$file =~ s/\/\//\//g;
#split the dir and the filename
my $dir = $file;
$dir =~ s/\/[^\/]*$//;
my $err;
eval {
#mkdir -p
_mkdir_p($dir);
die "couldn't make/walk directories: $@" if($@);
#touch/write to the file
open(FH, ">$file") || die "couldn't open $file: $!";
if($output) {
print FH $output || die "couldn't print $output to $file: $!";
}
close(FH) || die "couldn't close $file: $!";
chmod 0666, $file;
}; if($@) {
_log(2, "Cache::Static::_mkdirs_and_touch: couldn't update timestamps: $@");
}
}
sub _log {
my $severity = shift;
return unless($severity <= $CONF{log_level});
my $args = join(' ', @_);
$args =~ s/\n/ /mg;
$args =~ s/\s+$//;
#we don't need a full stack trace at level 3
#TODO: this regexp can be overly greedy
$args =~ s/Stack:.*$//sg if($CONF{log_level} == 3);
my @lt = localtime();
$lt[4]++; #month starts at 0 for perl, 1 for humans
@lt = map { sprintf("%02d", $_) } @lt;
my $date = ($lt[5]+1900).'/'.$lt[4].'/'.$lt[3].' '.$lt[2].':'.$lt[1].':'.$lt[0];
my $level = $LOG_LEVEL_NAMES[$severity];
$level .= ' ' while(length($level) < 5);
if($have_fcntl) {
#TODO: we don't need to open/close every time.
#just flock(LOG, LOCK_EX), seek, flock(LOG, LOCK_UN);
#benchmark and safety test this...
open(LOG, ">>$LOGFILE") || die "can't open log \"$LOGFILE\" $!";
flock(LOG, LOCK_EX) || die "can't lock log \"$LOGFILE\" $!";
seek(LOG, 0, 2); #seek to EOF if someone appended while we waited...
print LOG "$level $date [$$] $args\n" || die "can't write to log \"$LOGFILE\": $!";
#close does implicit unlock
close(LOG) || die "can't close log \"$LOGFILE\": $!";
} else {
#TODO: there must be a way to escape " such that the shell doesn't puke
$args =~ s/\"/'/g;
`echo "$level $date [$$] $args" >>$LOGFILE`;
}
}
sub _mkdir_p {
my $dir = shift;
my @dirs = grep (/./, split(/\//, $dir));
my $dir_so_far = '/';
foreach my $d (@dirs) {
$dir_so_far .= "$d/";
unless(-e $dir_so_far) {
mkdir($dir_so_far) || die "couldn't create $dir_so_far: $!";
chmod(0777, $dir_so_far) || die "couldn't change perms on $dir_so_far: $!";
}
}
}
1;
__END__
=head1 NAME
Cache::Static - Caching without freshness concerns
=head1 SYNOPSIS
=head2 HTML::Mason instructions
In handler.pl:
use Cache::Static;
In any component you where you have a well defined set of
dependencies which change the output:
<%init>
my $_cs_deps = [
#file dependencies - only regenerate if a file has changed
'file|/path/to/some_configuration_file',
#DBI dependencies - still under development - WONT WORK
#DBI dependencies: note the third argument is a DSN
'_DBI|table|mysql:scache_test_db|test_table',
'_DBI|db|mysql:scache_test_db',
#not yet implemented:
#column level depends, e.g. "DBI|column|$dsn|$tablename|$columname"
#row depends, e.g. "DBI|row|$dsn|$tablename|$uid_column_name|$uid_value"
#XML::Comma dependencies - only regenerate if a Doc or Store has changed
"_XML::Comma|Doc|$doc_key",
"_XML::Comma|Store|$def|$store",
#time dependencies (WARNING: these are discouraged, see doc/NOTE-time-deps)
'time|15s', #every 15 seconds
'time|M:15s', #every 15 seconds after the minute
'time|H:2m', #every 2 minutes past the hour
'time|W:2d3h5m0s', #every Tuesday at 3:05 AM
#modifiers (indicate behavior when the file cannot be found)
'file-0|/tmp/foo', #if ! -e /tmp/foo, regenerate
'file-1|/tmp/foo', #if ! -e /tmp/foo, serve
'file|/tmp/foo', #use config value "dep_file_not_found_returns"
#note modifiers also work on extensions, e.g.
'_DBI-1|db|mysql:scache_test_db',
'_XML::Comma-0|Store|mm_item|post',
#etc... but modifiers CANNOT be used with times (since they have no
#file backing on disk)
];
#whatever you have in $_cs_deps above...
( run in 0.487 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )