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); } }