siteKeyName = 'site_serial:' . crc32(SQL_TYPE . '://' . SQL_USER . ':' . SQL_PASS . '@' . SQL_SERVER . ':' . TABLE_PREFIX . ':' . SQL_DB);
// get cache handler class to use
$handler_class = kUtil::getSystemConfig()->get('CacheHandler', '') . 'CacheHandler';
// defined cache handler doesn't exist -> use default
if ( !class_exists($handler_class) ) {
$handler_class = 'FakeCacheHandler';
}
$handler = new $handler_class($this);
/* @var $handler FakeCacheHandler */
if ( !$handler->isWorking() ) {
// defined cache handler is not working -> use default
trigger_error('Failed to initialize "' . $handler_class . '" caching handler.', E_USER_WARNING);
$handler = new FakeCacheHandler($this);
}
elseif ( $this->Application->isDebugMode() && ($handler->getCachingType() == CACHING_TYPE_MEMORY) ) {
$this->Application->Debugger->appendHTML('Memory Caching: "' . $handler_class . '"');
}
$this->_handler = $handler;
$this->cachingType = $handler->getCachingType();
$this->debugCache = $handler->getCachingType() == CACHING_TYPE_MEMORY && $this->Application->isDebugMode();
$this->_storeStatistics = defined('DBG_CACHE') && DBG_CACHE;
if ( $this->_storeStatistics ) {
// don't use FileHelper, since kFactory isn't ready yet
if ( !file_exists(RESTRICTED . DIRECTORY_SEPARATOR . 'cache_usage') ) {
mkdir(RESTRICTED . DIRECTORY_SEPARATOR . 'cache_usage');
}
}
}
/**
* Returns caching type of current storage engine
*
* @return int
*/
function getCachingType()
{
return $this->cachingType;
}
/**
* Stores value to cache
*
* @param string $name
* @param mixed $value
* @param int $expiration cache record expiration time in seconds
* @return bool
*/
function setCache($name, $value, $expiration)
{
// 1. stores current version of serial for given cache key
$this->_setCache($name . '_serials', $this->replaceSerials($name), $expiration);
$this->storeStatistics($name, 'W');
// 2. don't replace serials within the key
$saved = $this->_setCache($name, $value, $expiration);
$this->storeStatistics($name, 'U');
// 3. remove rebuilding mark
$this->delete($name . '_rebuilding');
return $saved;
}
/**
* Stores value to cache
*
* @param string $name
* @param mixed $value
* @param int $expiration cache record expiration time in seconds
* @return bool
*/
function _setCache($name, $value, $expiration)
{
$prepared_name = $this->prepareKeyName($name);
$this->_localStorage[$prepared_name] = $value;
return $this->_handler->set($prepared_name, $value, $expiration);
}
/**
* Stores value to cache (only if it's not there already)
*
* @param string $name
* @param mixed $value
* @param int $expiration cache record expiration time in seconds
* @return bool
*/
function addCache($name, $value, $expiration)
{
// 1. stores current version of serial for given cache key
$this->_setCache($name . '_serials', $this->replaceSerials($name), $expiration);
// 2. remove rebuilding mark
$this->delete($name . '_rebuilding');
// 3. don't replace serials within the key
return $this->_addCache($name, $value, $expiration);
}
/**
* Stores value to cache (only if it's not there already)
*
* @param string $name
* @param mixed $value
* @param int $expiration cache record expiration time in seconds
* @return bool
*/
function _addCache($name, $value, $expiration)
{
$prepared_name = $this->prepareKeyName($name);
$added = $this->_handler->add($prepared_name, $value, $expiration);
if ( $added ) {
$this->_localStorage[$prepared_name] = $value;
}
return $added;
}
/**
* Sets rebuilding mode for given cache
*
* @param string $name
* @param int $mode
* @param int $max_rebuilding_time
* @param string $miss_type
* @return bool
*/
function rebuildCache($name, $mode = null, $max_rebuilding_time = 0, $miss_type = 'M')
{
if ( !isset($mode) || $mode == self::REBUILD_NOW ) {
$this->storeStatistics($name, $miss_type);
if ( !$max_rebuilding_time ) {
return true;
}
// prevent parallel rebuild attempt by using "add" instead of "set" method
if ( !$this->_addCache($name . '_rebuilding', 1, $max_rebuilding_time) ) {
$this->storeStatistics($name, 'l');
return false;
}
$this->storeStatistics($name, 'L');
$this->delete($name . '_rebuild');
}
elseif ( $mode == self::REBUILD_LATER ) {
$this->_setCache($name . '_rebuild', 1, 0);
$this->delete($name . '_rebuilding');
}
return true;
}
/**
* Returns value from cache
*
* @param string $name
* @param bool $store_locally store data locally after retrieved
* @param int $max_rebuild_seconds
* @return mixed
*/
function getCache($name, $store_locally = true, $max_rebuild_seconds = 0)
{
$cached_data = $this->_getCache(Array ($name . '_rebuild', $name . '_serials'), Array (true, true));
if ( $cached_data[$name . '_rebuild'] ) {
// cache rebuild requested -> rebuild now
$this->delete($name . '_rebuild');
if ( $this->rebuildCache($name, self::REBUILD_NOW, $max_rebuild_seconds, '[M1]') ) {
return false;
}
}
// There are 2 key types:
// - with serials, e.g. with_serial_key[%LangSerial%]
// - without serials, e.g. without_serial
// Evaluated serials of each cache key are stored in '{$name}_serials' cache key.
// If cache is present, but serial is outdated, then cache value is assumed to be outdated.
$new_serial = $this->replaceSerials($name);
$old_serial = $cached_data[$name . '_serials'];
if ( $name == $new_serial || $new_serial != $old_serial ) {
// no serials in cache key OR cache is outdated
$wait_seconds = $max_rebuild_seconds;
while (true) {
$cached_data = $this->_getCache(Array ($name, $name . '_rebuilding'), Array ($store_locally, false));
$cache = $cached_data[$name];
$rebuilding = $cached_data[$name . '_rebuilding'];
if ( ($cache === false) && (!$rebuilding || $wait_seconds == 0) ) {
// cache missing and nobody rebuilding it -> rebuild; enough waiting for cache to be ready
$this->rebuildCache($name, self::REBUILD_NOW, $max_rebuild_seconds, '[M2' . ($rebuilding ? 'R' : '!R') . ',WS=' . $wait_seconds . ']');
return false;
}
elseif ( $cache !== false ) {
// re-read serial, since it might have been changed in parallel process !!!
$old_serial = $this->_getCache($name . '_serials', false);
// cache present (if other user is rebuilding it, then it's outdated cache) -> return it
if ( $rebuilding || $new_serial == $old_serial ) {
$this->storeStatistics($name, $rebuilding ? 'h' : 'H');
return $cache;
}
$this->rebuildCache($name, self::REBUILD_NOW, $max_rebuild_seconds, '[M3' . ($rebuilding ? 'R' : '!R') . ',WS=' . $wait_seconds . ']');
return false;
}
$wait_seconds -= self::WAIT_STEP;
sleep(self::WAIT_STEP);
}
}
$cache = $this->_getCache($name, $store_locally);
if ( $cache === false ) {
$this->rebuildCache($name, self::REBUILD_NOW, $max_rebuild_seconds, '[M4]');
}
else {
$this->storeStatistics($name, 'H');
}
return $cache;
}
/**
* Returns cached value from local cache
*
* @param string $prepared_name Prepared key name from kCache::prepareKeyName() function
* @return mixed
* @see prepareKeyName
* @access public
*/
public function getFromLocalStorage($prepared_name)
{
return array_key_exists($prepared_name, $this->_localStorage) ? $this->_localStorage[$prepared_name] : false;
}
/**
* Returns value from cache
*
* @param string|Array $names
* @param bool|Array $store_locally store data locally after retrieved
* @return mixed
*/
function _getCache($names, $store_locally = true)
{
static $request_number = 1;
$res = Array ();
$names = (array)$names;
$store_locally = (array)$store_locally;
$to_get = $prepared_names = array_map(Array (&$this, 'prepareKeyName'), $names);
foreach ($prepared_names as $index => $prepared_name) {
$name = $names[$index];
if ( $store_locally[$index] && array_key_exists($prepared_name, $this->_localStorage) ) {
$res[$name] = $this->_localStorage[$prepared_name];
unset($to_get[$index]);
}
}
if ( $to_get ) {
$multi_res = $this->_handler->get($to_get);
foreach ($to_get as $index => $prepared_name) {
$name = $names[$index];
if ( array_key_exists($prepared_name, $multi_res) ) {
$res[$name] =& $multi_res[$prepared_name];
}
else {
$res[$name] = false;
}
$this->_postProcessGetCache($prepared_name, $res[$name], $store_locally[$index], $request_number);
}
$request_number++;
}
return count($res) == 1 ? array_pop($res) : $res;
}
/**
* Stores variable in local cache & collects debug info about cache
*
* @param string $name
* @param mixed $res
* @param bool $store_locally
* @param int $request_number
* @return void
* @access protected
*/
protected function _postProcessGetCache($name, &$res, $store_locally = true, $request_number)
{
if ( $this->debugCache ) {
// don't display subsequent serial cache retrievals (ones, that are part of keys)
if ( is_array($res) ) {
$this->Application->Debugger->appendHTML('r' . $request_number . ': Restoring key "' . $name . '". Type: ' . gettype($res) . '.');
}
else {
$res_display = strip_tags($res);
if ( strlen($res_display) > 200 ) {
$res_display = substr($res_display, 0, 50) . ' ...';
}
$this->Application->Debugger->appendHTML('r' . $request_number . ': Restoring key "' . $name . '" resulted [' . $res_display . ']');
}
}
if ( $store_locally /*&& ($res !== false)*/ ) {
$this->_localStorage[$name] = $res;
}
}
/**
* Deletes value from cache
*
* @param string $name
* @return mixed
*/
function delete($name)
{
$name = $this->prepareKeyName($name);
unset($this->_localStorage[$name]);
return $this->_handler->delete($name);
}
/**
* Reset's all memory cache at once
*/
function reset()
{
// don't check for enabled, because we maybe need to reset cache anyway
if ($this->cachingType == CACHING_TYPE_TEMPORARY) {
return ;
}
$site_key = $this->_cachePrefix(true);
$this->_handler->set($site_key, $this->_handler->get($site_key) + 1);
}
/**
* Replaces serials and adds unique site prefix to cache variable name
*
* @param string $name
* @return string
*/
protected function prepareKeyName($name)
{
if ( $this->cachingType == CACHING_TYPE_TEMPORARY ) {
return $name;
}
// add site-wide prefix to key
return $this->_cachePrefix() . $name;
}
/**
* Replaces serials within given string
*
* @param string $value
* @return string
* @access protected
*/
protected function replaceSerials($value)
{
if ( preg_match_all('/\[%(.*?)%\]/', $value, $regs) ) {
// [%LangSerial%] - prefix-wide serial in case of any change in "lang" prefix
// [%LangIDSerial:5%] - one id-wide serial in case of data, associated with given id was changed
// [%CiIDSerial:ItemResourceId:5%] - foreign key-based serial in case of data, associated with given foreign key was changed
$serial_names = $regs[1];
$serial_count = count($serial_names);
$store_locally = Array ();
for ($i = 0; $i < $serial_count; $i++) {
$store_locally[] = true;
}
$serial_values = $this->_getCache($serial_names, $store_locally);
if ( !is_array($serial_values) ) {
$serial_values = Array (current($serial_names) => $serial_values);
}
foreach ($serial_names as $serial_name) {
$value = str_replace('[%' . $serial_name . '%]', '[' . $serial_name . '=' . $serial_values[$serial_name] . ']', $value);
}
}
return $value;
}
/**
* Returns site-wide caching prefix
*
* @param bool $only_site_key_name
* @return string
*/
function _cachePrefix($only_site_key_name = false)
{
if ($only_site_key_name) {
return $this->siteKeyName;
}
if ( !isset($this->siteKeyValue) ) {
$this->siteKeyValue = $this->_handler->get($this->siteKeyName);
if (!$this->siteKeyValue) {
$this->siteKeyValue = 1;
$this->_handler->set($this->siteKeyName, $this->siteKeyValue);
}
}
return "{$this->siteKeyName}:{$this->siteKeyValue}:";
}
/**
* Stores statistics about cache usage in a file (one file per cache)
*
* @param string $name
* @param string $action_type {M - miss, L - lock, W - write, U - unlock, H - actual hit, h - outdated hit}
* @return void
* @access public
*/
public function storeStatistics($name, $action_type)
{
if ( !$this->_storeStatistics ) {
return;
}
$name = str_replace(Array ('/', '\\', ':'), '_', $name);
$fp = fopen(RESTRICTED . DIRECTORY_SEPARATOR . 'cache_usage' . DIRECTORY_SEPARATOR . $name, 'a');
fwrite($fp, $action_type);
fclose($fp);
}
}
abstract class kCacheHandler {
/**
* Remembers status of cache handler (working or not)
*
* @var bool
* @access protected
*/
protected $_enabled = false;
/**
* Caching type that caching handler implements
*
* @var int
* @access protected
*/
protected $cachingType;
/**
*
* @var kCache
* @access protected
*/
protected $parent;
public function __construct(kCache $parent)
{
$this->parent = $parent;
}
/**
* Retrieves value from cache
*
* @param string $names
* @return mixed
* @access public
*/
abstract public function get($names);
/**
* Stores value in cache
*
* @param string $name
* @param mixed $value
* @param int $expiration
* @return bool
* @access public
*/
abstract public function set($name, $value, $expiration = 0);
/**
* Stores value in cache (only if it's not there already)
*
* @param string $name
* @param mixed $value
* @param int $expiration
* @return bool
* @access public
*/
abstract public function add($name, $value, $expiration = 0);
/**
* Deletes key from cach
*
* @param string $name
* @return bool
* @access public
*/
abstract public function delete($name);
/**
* Determines, that cache storage is working fine
*
* @return bool
* @access public
*/
public function isWorking()
{
return $this->_enabled;
}
/**
* Returns caching type of current storage engine
*
* @return int
* @access public
*/
public function getCachingType()
{
return $this->cachingType;
}
}
class FakeCacheHandler extends kCacheHandler {
public function __construct(kCache $parent)
{
parent::__construct($parent);
$this->_enabled = true;
$this->cachingType = CACHING_TYPE_TEMPORARY;
}
/**
* Retrieves value from cache
*
* @param string|Array $names
* @return mixed
* @access public
*/
public function get($names)
{
if ( is_array($names) ) {
$res = Array ();
foreach ($names as $name) {
$res[$name] = $this->parent->getFromLocalStorage($name);
}
return $res;
}
return $this->parent->getFromLocalStorage($names);
}
/**
* Stores value in cache
*
* @param string $name
* @param mixed $value
* @param int $expiration
* @return bool
* @access public
*/
public function set($name, $value, $expiration = 0)
{
return true;
}
/**
* Stores value in cache (only if it's not there already)
*
* @param string $name
* @param mixed $value
* @param int $expiration
* @return bool
* @access public
*/
public function add($name, $value, $expiration = 0)
{
return true;
}
/**
* Deletes key from cach
*
* @param string $name
* @return bool
* @access public
*/
public function delete($name)
{
return true;
}
}
class MemcacheCacheHandler extends kCacheHandler {
/**
* Memcache connection
*
* @var Memcache
* @access protected
*/
protected $_handler = null;
public function __construct(kCache $parent, $default_servers = '')
{
parent::__construct($parent);
$this->cachingType = CACHING_TYPE_MEMORY;
$memcached_servers = kUtil::getSystemConfig()->get('MemcacheServers', $default_servers);
if ( $memcached_servers && class_exists('Memcache') ) {
$this->_enabled = true;
$this->_handler = new Memcache();
$servers = explode(';', $memcached_servers);
foreach ($servers as $server) {
if ( preg_match('/(.*):([\d]+)$/', $server, $regs) ) {
// "hostname:port" OR "unix:///path/to/socket:0"
$server = $regs[1];
$port = $regs[2];
}
else {
$port = 11211;
}
$this->_handler->addServer($server, $port);
}
// verify, that memcache server is working
if ( !$this->_handler->set('test', 1) ) {
$this->_enabled = false;
}
}
}
/**
* Retrieves value from cache
*
* @param string $name
* @return mixed
* @access public
*/
public function get($name)
{
return $this->_handler->get($name);
}
/**
* Stores value in cache
*
* @param string $name
* @param mixed $value
* @param int $expiration
* @return bool
* @access public
*/
public function set($name, $value, $expiration = 0)
{
// 0 - don't use compression
return $this->_handler->set($name, $value, 0, $expiration);
}
/**
* Stores value in cache (only if it's not there already)
*
* @param string $name
* @param mixed $value
* @param int $expiration
* @return bool
* @access public
*/
public function add($name, $value, $expiration = 0)
{
// 0 - don't use compression
return $this->_handler->add($name, $value, 0, $expiration);
}
/**
* Deletes key from cache
*
* @param string $name
* @return bool
* @access public
*/
public function delete($name)
{
return $this->_handler->delete($name, 0);
}
}
class ApcCacheHandler extends kCacheHandler {
public function __construct(kCache $parent)
{
parent::__construct($parent);
$this->cachingType = CACHING_TYPE_MEMORY;
$this->_enabled = function_exists('apc_fetch');
// verify, that apc is working
if ( $this->_enabled && !$this->set('test', 1) ) {
$this->_enabled = false;
}
}
/**
* Retrieves value from cache
*
* @param string $name
* @return mixed
* @access public
*/
public function get($name)
{
return apc_fetch($name);
}
/**
* Stores value in cache
*
* @param string $name
* @param mixed $value
* @param int $expiration
* @return bool
* @access public
*/
public function set($name, $value, $expiration = 0)
{
return apc_store($name, $value, $expiration);
}
/**
* Stores value in cache (only if it's not there already)
*
* @param string $name
* @param mixed $value
* @param int $expiration
* @return bool
* @access public
*/
public function add($name, $value, $expiration = 0)
{
return apc_add($name, $value, $expiration);
}
/**
* Deletes key from cache
*
* @param string $name
* @return bool
* @access public
*/
public function delete($name)
{
return apc_delete($name);
}
}
class XCacheCacheHandler extends kCacheHandler {
public function __construct(kCache $parent)
{
parent::__construct($parent);
$this->cachingType = CACHING_TYPE_MEMORY;
$this->_enabled = function_exists('xcache_get');
// verify, that xcache is working
if ( $this->_enabled && !$this->set('test', 1) ) {
$this->_enabled = false;
}
}
/**
* Retrieves value from cache
*
* @param string|Array $names
* @return mixed
* @access public
*/
public function get($names)
{
if ( is_array($names) ) {
$res = Array ();
foreach ($names as $name) {
$res[$name] = $this->get($name);
}
return $res;
}
return xcache_isset($names) ? xcache_get($names) : false;
}
/**
* Stores value in cache
*
* @param string $name
* @param mixed $value
* @param int $expiration
* @return bool
* @access public
*/
public function set($name, $value, $expiration = 0)
{
return xcache_set($name, $value, $expiration);
}
/**
* Stores value in cache (only if it's not there already)
*
* @param string $name
* @param mixed $value
* @param int $expiration
* @return bool
* @access public
*/
public function add($name, $value, $expiration = 0)
{
// not atomic operation, like in Memcached and may fail
if ( xcache_isset($name) ) {
return false;
}
return $this->set($name, $value, $expiration);
}
/**
* Deletes key from cache
*
* @param string $name
* @return bool
* @access public
*/
public function delete($name)
{
return xcache_unset($name);
}
}