> > So, are you interested in PHP realization with main idea explained?
> Yes

Ok, hope it will be interesting:

In my CMS there is (there was?) such an abstraction: node. Node - it's
everything - it's a news, article or any other content unit.

Node has all CRUD operations through the functions: get, add, edit,
delete.

There is also such a module - cache: it track two things - dependence
and time. Dependence, in fact, that's actually, one file, which is
`touch`ed automaticly every time, any non-read node operations is
perfomed (it is just a cache_update_dependence() call from node
functions).

Then, any page, which may want to use cache, has the following lines
(it can be put in one place/class, in a normal designed,
object-oriented, enviroment):
============
//Predefine cache data
$CacheID = basename($_SERVER['SCRIPT_NAME']);
$CurrTs = time(); $CacheExpireTs =  mktime(0, 0, 1, date('m', $CurrTs),
(date('d', $CurrTs) + 1), date('Y', $CurrTs)); //first second of the
next day
$CacheDependencies = array('taxonomy_data', 'taxonomy_function',
'taxonomy_hierarchy', 'taxonomy_nodes', 'source_data', 'source_nodes',
'nodes');} //show nid

#Check cache
if ($CachedContent = cache_get($CacheID, $ScriptArgs,
$CacheDependencies)) {echo $CachedContent;} //Return cached page
content
else { //Get and construct page content
   //content generation here
}//if($CachedContent
============

Dependencies - it's just an empty files, with the same names, as an
existen tables.

What cache do behind the scene:
- it check, cache validness: if table was modified (and touched by the
node functions) recently or if cache should be deleted by time-out - it
is droped and Null is returned; otherwise - cache content, which is
readen from the file, which name is an md5 sum from the script
arguments (special function for that).
- it check cache existens;
- it also empty cache, by cron, by calling cache_clean() function,
which delete all cache object, starting from the oldest one, untill
predefined (in settings) size will be reached (so we will always have
the most recent XXX Mbytes of cache).

cache.inc source code below.
Please, do not blame me too much - yes, this is not an object oriented
code, and it is not perfect, by I just want, that the idea of such a
cache will not dyu with the code (when it was the time, when I need a
cache, I do not find any cache sollutions based on the DB changes
track, so I have to invent it on my own).

cache.inc (comments were written once and was not modified with the
code updates)
==================================================================
<?php
/* Work algorithm: track cache validness using expiration time & (db)
dependencies
Rules:
        - cache is decided to be dirty if cache timestamp is invalid _or_ if
one of the dependencies is out of the date
        - correct dependencies is a caller problem and should be specified at
the cache_get function
        - database tables (dependencies) update is core functions problem
        - the only thing cache take on itself - it's section handle (including
table (dep) completion: 'taxonomy_data' dep will be completed to the
'SECTION._taxonomy_data' )

Some details: expiration is computed using file modification time
(cache expire timestamp == file modification time), so, cache creation
time is not available.
Algorithm:
        - two stuff is tracked: cache expire time & dependence update time
        - cache has three dates: creation, last acces and expire, that is
inode change time of file, last file access time and file modification
accordingly
        - dependencies tracked in a following algorithm: if dep. access time
is more than cache creation, than cache is obsolete, and it's dropped
*/
//ToDo:
// - tune tables updates track
// - implement function cache

ini_set('ignore_user_abort', 1); //partly loaded pages is useless and
should never be in cache (suppose, that if script is failed, than it
will never store cache, cause it's one of the last deal)

//Convert function args to the fs storable hash (md5 used);
func_get_args - for function and script_args for script should be used;
valid for no more than a two level array.
function args_hash($Args = NULL) {
        if (!is_array($Args) && !is_string($Args)) {global $ErrMsg;
func_error_handler(array(__FUNCTION__, func_get_args(),
$ErrMsg['InvalidArgs'])); return FALSE;}

        if (is_string($Args)) {$ArgsString = $Args;} //just a line
        else if (is_array($Args)) {
                $ArgsString = '';
                while(list($Key, $Val) = each($Args)) { //no more than a two 
level
array reconstructor
                        if (!is_array($Val)) {$ArgsString .= $Key.$Val;}
                        else if (is_array($Val)) {while(list($SubKey, $SubVal) 
= each($Val))
{$ArgsString .= $SubKey.$SubVal;}}
                }//while
        } else {
                func_error_handler(array(__FUNCTION__, func_get_args(),
$ErrMsg['InvalidArgs'])); return FALSE;
        }//if(is_string

return md5($ArgsString);
}//function


//Just check enviroment to the errors; return FALSE & log event, that
may "polute" cache
function cache_env_dirty() {

        if ($ErrorMessage = error_description_read()) {
                debug_log('Cache', __FUNCTION__.": cache interrupted because of 
the
unhandled error.");
                $GLOBALS['CashSwitch'] = 0;
                return $ErrorMessage;
        } else {
                return FALSE;
        } //if($ErrorMessage

}//function


//Should be called by any function, that insert/update any info to the
database; section append is optional, cause other db function know full
table name
function cache_update_dependence($Dependence = NULL, $SectionAppend =
0) {
        global $CacheStatDir;

        if ($SectionAppend) {if (!defined('SECTION')) {$Section =
'UnSectioned';} else {$Section = strtolower(SECTION);}}

        if (!is_string($Dependence) || (($SectionAppend != 0) &&
($SectionAppend != 1))) {global $ErrMsg;
func_error_handler(array(__FUNCTION__, func_get_args(),
$ErrMsg['InvalidArgs'])); return FALSE;}

        if (!is_dir($CacheStatDir)) { //dependence dir should exist allready
                global $ErrMsg;
                func_error_handler(array(__FUNCTION__, func_get_args(),
"$CacheStatDir {$ErrMsg['UnexistenDirectory']}.
{$ErrMsg['CachePreventCorruptionAndOff']}"));
                $GLOBALS['CashSwitch'] = 0;
                return FALSE;
        }//if(!is_dir

        $DependenceFullName =
($SectionAppend)?"{$Section}_{$Dependence}":$Dependence;
        $DependenceFullPath = "$CacheStatDir/$DependenceFullName";

        $CurrTime = time();
        $Result = touch($DependenceFullPath, $CurrTime, $CurrTime);

        if ($Result) {
                debug_log('Cache', __FUNCTION__.": '$Dependence' dependence 
updated;
access & modification time is set to ".timestamp2date($CurrTime, 1, '
')." ($CurrTime)");
                return TRUE;
        } else {
                global $ErrMsg;
                func_error_handler(array(__FUNCTION__, func_get_args(), 
"$Dependence
{$ErrMsg['DepUpdateFail']}.
{$ErrMsg['CachePreventCorruptionAndOff']}"));
                $GLOBALS['CashSwitch'] = 0;
                return FALSE;
        }//if($Result

}//function


//===>> Function store_page_cache <<===
function cache_store($Item = NULL, $Args = NULL, $Content = NULL,
$Expire = NULL) {
        global $CacheFilesRoot, $ErrMsg;
        settype($Expire, 'int');

        if (!defined('SECTION')) {$Section = 'UnSectioned';} else {$Section =
strtolower(SECTION);}

        //Arguments check
        if (!is_string($Item) || (!is_string($Args) && !is_array($Args)) ||
!is_string($Content) || !is_int($Expire))
{func_error_handler(array(__FUNCTION__, func_get_args(),
$ErrMsg['InvalidArgs'])); return FALSE;}

        //Check unhandled errors
        if ($UnhandledError = cache_env_dirty())
{func_error_handler(array(__FUNCTION__, func_get_args(),
"[i]'$UnhandledError'[/i].
{$ErrMsg['CachePreventCorruptionAndOff']}.")); return FALSE;}

        //Global cache switch check
        if (!$GLOBALS['CashSwitch']) {return FALSE;}

        $ArgsHash = args_hash($Args);
        $ItemDir = "$CacheFilesRoot/$Section/$Item";
        $TargetFile = "$ItemDir/$ArgsHash";

        //Destination dir check
        if (!is_dir($ItemDir)) {if (!mk_r_dir($ItemDir))
{func_error_handler(array(__FUNCTION__, func_get_args(), "$ItemDir
{$ErrMsg['DirCantBeCreated']}")); return FALSE;}}

        if (file_store($TargetFile, $Content) && touch($TargetFile, $Expire))
{
                debug_log('Cache', __FUNCTION__."($Item, $ArgsHash, content 
(length):
".strlen($Content).", expire: ".timestamp2date($Expire, 1, ' ')."
($Expire)) - done.");
                return TRUE; //content stored, cach marked - all is Ok
        } else {
                func_error_handler(array(__FUNCTION__, func_get_args(), 
"$TargetFile
{$ErrMsg['FileCantBeCreatedUpdated']}")); return FALSE;
        }//if(file_store

}//function


//Return cache
function cache_get($Item = NULL, $Args = NULL, $Dependencies = array())
{
        global $CacheFilesRoot, $CacheStatDir;

        $FuncArgs = func_get_args(); debug_log('Cache',
__FUNCTION__."(".array2string($FuncArgs).")");

        if (!defined('SECTION')) {$Section = 'UnSectioned';} else {$Section =
strtolower(SECTION);}

        if (!is_string($Item) || (!is_string($Args) && !is_array($Args)) ||
!is_array($Dependencies)) {global $ErrMsg;
func_error_handler(array(__FUNCTION__, func_get_args(),
$ErrMsg['InvalidArgs'])); return FALSE;}

        if (!$GLOBALS['CashSwitch']) {return FALSE;} //cache is switched off

        $ArgsHash = args_hash($Args);
        $DependenciesAmount = count($Dependencies);
        $Dir = $CacheFilesRoot."/".$Section."/".$Item;
        $CacheFile = $Dir."/".$ArgsHash;

        if (!is_dir($Dir) || !is_file($CacheFile)) {debug_log('Cache',
__FUNCTION__."($Item, $ArgsHash, $DependenciesAmount) there is no cache
file: '$CacheFile'"); return FALSE;}

        debug_log('Cache', __FUNCTION__."('$Item', '$ArgsHash',
'$DependenciesAmount') - file '$CacheFile'");

        $CheckingCurrTimestamp = time();
        $CacheExpirationTs = filemtime($CacheFile);

        if ($CacheExpirationTs <= $CheckingCurrTimestamp) {
                debug_log('Cache', __FUNCTION__."($Item, $ArgsHash,
$DependenciesAmount) cache expired at
".timestamp2date($CacheExpirationTs, 1, ' '));
                cache_drop($Item, $ArgsHash);
                return FALSE;
        } else if ($DependenciesAmount) {

                if (!is_dir($CacheStatDir)) { //dependence dir should exist 
allready
                        func_error_handler(array(__FUNCTION__, func_get_args(),
"$CacheStatDir {$ErrMsg['UnexistenDirectory']}.
{$ErrMsg['CachePreventCorruptionAndOff']}"));
                        $GLOBALS['CashSwitch'] = 0;
                        return FALSE;
                }//if(!is_dir

                //Return FALSE, if any of the dep is updated
                for ($i = 0; $i < $DependenciesAmount; $i++) {
                        $CurrDep = 
"$CacheStatDir/{$Section}_{$Dependencies[$i]}";
                        $CacheCreationTs = filectime($CacheFile);
                        $CurrDepUpdateTs = fileatime($CurrDep);
                        if ($CacheCreationTs <= $CurrDepUpdateTs) {
                                debug_log('Cache', __FUNCTION__."($Item, 
$ArgsHash,
$DependenciesAmount) $CurrDep dependence expired at
".timestamp2date($CurrDepUpdateTs, 1, ' '));
                                cache_drop($Item, $ArgsHash);
                                return FALSE;
                        }//if(filemtime
                }//for

        }//if($DependenciesAmount

        //If all about code is passed, then expire & dependencies is Ok
        debug_log('Cache', __FUNCTION__."($Item, $ArgsHash,
$DependenciesAmount) returning cache content");
return file_read($CacheFile);
}//function

//===>> Function drop_page_cache <<===
function cache_drop($Item = NULL, $Args = NULL) {
        global $CacheFilesRoot;

        if (!defined('SECTION')) {$Section = 'UnSectioned';} else {$Section =
strtolower(SECTION);}

        if (!is_string($Item) || (!is_string($Args) && !is_array($Args)))
{global $ErrMsg; func_error_handler(array(__FUNCTION__,
func_get_args(), $ErrMsg['InvalidArgs'])); return FALSE;}

        $ArgsHash = args_hash($Args);

        $Dir = $CacheFilesRoot."/".$Section."/".$Item;
        $CacheFile = $Dir."/".$ArgsHash;

        debug_log('Cache', __FUNCTION__."($Item, $ArgsHash) dropping cache");
return file_delete($CacheFile);
}//function

//Do not forget to clear vars, before reusing
function dir_size_atime_info($DirName = NULL) {
        if (!is_string($DirName)) {global $ErrMsg;
func_error_handler(array(__FUNCTION__, func_get_args(),
$ErrMsg['InvalidArgs'])); return FALSE;}

        static $FullSize = 0, $Amount = 0, $Id = 0, $Files = array();

        //static vars has to be cleaned by hand
        if ($DirName == 'CLEARVARS') {
                $FullSize = 0; $Amount = 0; $Id = 0; $Files = array();
//              debug_log('Cache', __FUNCTION__.": variables cleared [$FullSize,
$Amount, $Id, ".str_replace("\n", '', print_r($Files, 1))."]");
                return TRUE;
        }//if($DirName

        if (!is_dir($DirName)) {return array('amount' => $Amount, 'size' =>
$FullSize, 'data' => $Files);}

        if (!$DirHandle = opendir($DirName)) {global $ErrMsg;
func_error_handler(array(__FUNCTION__, func_get_args(), "$DirName
{$ErrMsg['FileCantBeOpened']}")); return FALSE;}
        while (FALSE != ($File = readdir($DirHandle))) {
                $FullFilePath = $DirName.'/'.$File;
                if (($File != '.') && ($File != '..') && 
!is_link($FullFilePath)) {
                        if (is_dir($FullFilePath)) {
                                dir_size_atime_info($FullFilePath);
                        } else if(is_file($FullFilePath))  {
                                $FileStatInfo = stat($FullFilePath);
                                $Files['file'][$Id] = $FullFilePath; //file
                                $Files['size'][$Id] = $FileStatInfo['size']; 
//size
                                $Files['atime'][$Id] = $FileStatInfo['atime']; 
//access time
                                $Id++; $Amount++; $FullSize += 
$FileStatInfo['size'];
//                              debug_log('Cache', __FUNCTION__." full size: 
$FullSize");
                                unset($FileStatInfo);
                        }//if
                }//if
        } //while (skip '.', '..' and links)
        closedir($DirHandle);

return array('amount' => $Amount, 'size' => $FullSize, 'data' =>
$Files);
}//function

function cache_clean($SectionToClean = NULL) {
        if (!is_string($SectionToClean)) {global $ErrMsg;
func_error_handler(array(__FUNCTION__, func_get_args(),
$ErrMsg['InvalidArgs'])); return FALSE;}
        global $CacheSize, $CacheFilesRoot;

        $FullPath = $CacheFilesRoot.'/'.$SectionToClean;

        $NodeLimit = mbytes_to_bytes($CacheSize[$SectionToClean]);
        $NodeCacheInfo = dir_size_atime_info($FullPath);
//      debug_log('Cache', __FUNCTION__."() accepted
{$NodeCacheInfo['amount']} files; size: {$NodeCacheInfo['size']}
(".bytes_to_mbytes($NodeCacheInfo['size'])." Mb).");
        dir_size_atime_info('CLEARVARS');

        $Counter = 0;
        $Odds = $NodeLimit - $NodeCacheInfo['size'];

        if (($Odds <= 0) && ($NodeCacheInfo['size'] != 0)) {
                $Odds *= -1;
                debug_log('Cache', "[$SectionToClean] ".'Limit exceed for 
'.$Odds.'
bytes ('.bytes_to_mbytes($Odds).' Mb), '.$NodeCacheInfo['amount'].'
elements to process (curr limit: '.bytes_to_mbytes($NodeLimit).' Mb,
curr occuped: '.bytes_to_mbytes($NodeCacheInfo['size']).' Mb)');

                asort($NodeCacheInfo['data']['atime']);

                for (; $Value = current($NodeCacheInfo['data']['atime']), $Odds 
> 0;
$Value = next($NodeCacheInfo['data']['atime']), $Counter++) {
                        $Key = key($NodeCacheInfo['data']['atime']); $Size =
$NodeCacheInfo['data']['size'][$Key]; $FileName =
$NodeCacheInfo['data']['file'][$Key];
                        if (file_delete($FileName)) {$Odds -= $Size;}
                        debug_log('Cache', "[$SectionToClean] Id: $Counter, 
key: ".$Key." =>
atime: $Value, size: $Size, file name: $FileName; odds: $Odds");
                }//for

        } else {
                debug_log('Cache', "[$SectionToClean] ".'There is cache space
('.bytes_to_mbytes($NodeCacheInfo['size']).' Mb, limit:
'.bytes_to_mbytes($NodeLimit).' Mb), no need to clean
('.$NodeCacheInfo['amount'].' files analyzed).');
        }//if

        clearstatcache();

return $Counter;
}//function
?>
==================================================================

Hope it will be usefull and will find some reflection in existen CMSs.
Please, feel free to ask me any questions regarding my CMS and such a
cache implementation.

Regards,
/Alexander.

Reply via email to